Skip to content

Refactor data handling of contacts#267

Merged
wel97459 merged 3 commits into
mainfrom
dev-contactFix
Mar 7, 2026
Merged

Refactor data handling of contacts#267
wel97459 merged 3 commits into
mainfrom
dev-contactFix

Conversation

@wel97459

@wel97459 wel97459 commented Mar 7, 2026

Copy link
Copy Markdown
Collaborator

Refactor data handling in MeshCoreConnector for contacts.
And improved BufferReader.

This fixes the routeTransportFlood and routeTransportDirect packet types not being parsed correctly.
Add reset and rewind to the BufferReader.

Copilot AI review requested due to automatic review settings March 7, 2026 09:11
@wel97459

wel97459 commented Mar 7, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee7fa14f96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/models/contact.dart
type: type,
flags: flags,
pathLength: pathLen,
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve direct path length when decoding contact frames

pathLen is read as an unsigned byte, but this assignment maps every pathLen <= 0 to -1, so a valid direct route (pathLen == 0) is now decoded as flood. That changes routing behavior (pathLength < 0 is treated as flood throughout the connector) and can break follow-up writes that pass this value back into buildUpdateContactPathFrame (which writes pathLen as a byte). Keep 0 as direct and only map the flood sentinel to -1.

Useful? React with 👍 / 👎.

Comment thread lib/models/contact.dart
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);

final lastMod = reader.readUInt32LE();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read contact last-seen from the correct offset

This refactor now stores lastSeen from the first 4-byte field after the name, but the protocol defines that field as contactTimestampOffset (132) and the real last-modified value at contactLastModOffset (144) in lib/connector/meshcore_protocol.dart. Because the parser no longer skips the timestamp field before assigning lastSeen, contact freshness/order can regress to stale or incorrect times.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors contact and RX packet parsing to use BufferReader consistently, and adjusts handling for transport route types so flood/direct packets are parsed correctly.

Changes:

  • Refactor Contact.fromFrame to parse sequentially with BufferReader and add error logging on parse failure.
  • Update RX parsing in MeshCoreConnector to skip transport-specific bytes for routeTransportFlood / routeTransportDirect.
  • Extend BufferReader with pointer tracking (rewind, resetPointer) and add a greedy C-string reader variant.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
lib/models/contact.dart Refactors contact frame parsing to use BufferReader and logs parsing failures.
lib/connector/meshcore_protocol.dart Enhances BufferReader with pointer rewind/reset and adds readCStringGreedy.
lib/connector/meshcore_connector.dart Fixes RX parsing for transport route types by skipping transport-specific bytes before reading path/payload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/connector/meshcore_connector.dart Outdated
Comment on lines +65 to +80
String readCString(int maxLength) {
_lastPointer = _pointer;
final value = <int>[];
int counter = 0;
while (counter < maxLength) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
counter++;
}
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readCString() sets _lastPointer at the start, but then calls readByte() in a loop; readByte() delegates to readBytes(), which overwrites _lastPointer on every iteration. As a result, rewind() after readCString() will only rewind a single byte (and the initial _lastPointer = _pointer in readCString() is effectively lost). If rewind() is meant to undo the whole readCString() call, preserve the starting offset (e.g. store it locally and restore _lastPointer at the end, or implement the scan using a single readBytes() call).

Copilot uses AI. Check for mistakes.
Comment thread lib/models/contact.dart
Comment on lines +180 to +185
final pathLen = reader.readByte();
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastModOffset);
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathLen is read as an unsigned byte, but contact frames previously treated it as a signed int8 (e.g. 0xFF => -1). With the current readByte() approach, a flood sentinel (0xFF) becomes 255, which makes safePathLen clamp to maxPathSize and populates pathBytes even when there should be no path. Read this as a signed value (e.g. readInt8() / .toSigned(8)) and keep safePathLen based on the signed value.

Copilot uses AI. Check for mistakes.
Comment thread lib/models/contact.dart
Comment on lines 200 to 207
type: type,
flags: flags,
pathLength: pathLen,
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
);

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathLength is being normalized to -1 when pathLen <= 0, which turns the direct case (pathLen == 0) into flood. This will break Contact.pathLabel and outgoing path selection (the code treats pathLength == 0 as direct and < 0 as flood). Preserve 0 for direct and only map negative values (and/or invalid > max) to flood.

Copilot uses AI. Check for mistakes.
name: name,
type: type,
pathLength: pathBytes.length,
pathLength: pathBytes.isEmpty ? -1 : pathBytes.length,

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathLength is set to -1 when pathBytes is empty, which reclassifies direct adverts (empty path, non-flood route) as flood. This also creates an inconsistency with _handlePayloadAdvertReceived, which later refreshes existing contacts using pathLength: path.length (0 for empty). Consider deriving flood vs direct from routeType (or keep pathLength: pathBytes.length and only force -1 when the route type is a flood variant).

Suggested change
pathLength: pathBytes.isEmpty ? -1 : pathBytes.length,
pathLength: pathBytes.length,

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 7, 2026 09:18
@wel97459 wel97459 merged commit c2671ac into main Mar 7, 2026
6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/models/contact.dart
final pubKey = reader.readBytes(pubKeySize);
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathLen is read as an unsigned byte (readByte()), but contact pathLength semantics in this codebase rely on a signed int8 (<0=flood, 0=direct, >0=hops). This makes flood routes impossible to represent and will also mis-handle values >= 128. Read this field as signed (e.g., readInt8() / .toSigned(8)) and keep safePathLen logic based on the signed value.

Suggested change
final pathLen = reader.readByte();
final pathLen = reader.readByte().toSigned(8);

Copilot uses AI. Check for mistakes.
Comment thread lib/models/contact.dart
type: type,
flags: flags,
pathLength: pathLen,
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pathLength mapping here converts pathLen == 0 (direct) into -1 (flood), which contradicts the pathLabel logic and model docs (-1=flood, 0=direct). Preserve 0 for direct, and only use <0 for flood (or derive flood/direct from the signed pathLen field).

Suggested change
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
pathLength: pathLen == 0 ? 0 : (pathLen > maxPathSize ? -1 : pathLen),

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +75
final backupPointer = _pointer;
final value = <int>[];
int counter = 0;
while (counter < maxLength) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
counter++;
}
_lastPointer = backupPointer;

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readCString() sets _lastPointer at the start, but then calls readByte() in a loop; readByte() updates _lastPointer on every iteration via readBytes(1). As a result, rewind() will jump back only one byte (or some mid-string position) instead of rewinding the whole CString read. Consider implementing readCString() without calling readByte()/readBytes() (read from _buffer[_pointer] directly) or ensure _lastPointer remains the starting pointer for the entire CString operation.

Suggested change
final backupPointer = _pointer;
final value = <int>[];
int counter = 0;
while (counter < maxLength) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
counter++;
}
_lastPointer = backupPointer;
// Mark the start of this logical read so rewind() can return here.
_lastPointer = _pointer;
final value = <int>[];
int counter = 0;
// Read directly from the underlying buffer to avoid updating _lastPointer
// on each byte via readByte()/readBytes().
while (counter < maxLength && _pointer < _buffer.length) {
final byte = _buffer[_pointer];
_pointer++;
counter++;
if (byte == 0) {
break;
}
value.add(byte);
}

Copilot uses AI. Check for mistakes.
return value;
}

void resetPointer() => _pointer = 0;

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetPointer() resets _pointer but leaves _lastPointer unchanged, so a subsequent rewind() after a reset can jump to an unrelated earlier offset. Resetting should likely also reset _lastPointer to 0 (or to the new _pointer) to keep rewind behavior consistent.

Suggested change
void resetPointer() => _pointer = 0;
void resetPointer() {
_pointer = 0;
_lastPointer = 0;
}

Copilot uses AI. Check for mistakes.
Comment on lines 3922 to 3924
type: type,
pathLength: pathBytes.length,
pathLength: pathBytes.isEmpty ? -1 : pathBytes.length,
path: Uint8List.fromList(

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting pathLength to -1 whenever pathBytes is empty loses the distinction between direct (0 hops) and flood (-1). Since routeType is already parsed above, consider setting pathLength based on routeType when pathBytes.isEmpty (e.g., direct -> 0, flood -> -1) to keep DiscoveryContact.pathLabel accurate.

Copilot uses AI. Check for mistakes.
@wel97459 wel97459 deleted the dev-contactFix branch March 7, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants