Skip to content

bug: buildUpdateContactPathFrame writes lat/lon twice, corrupting the optional timestamp tail #427

Description

@kingult

Bug

buildUpdateContactPathFrame in lib/connector/meshcore_protocol.dart:723-747 emits two consecutive 8-byte position blocks instead of one. The frame ends up 8 bytes longer than the documented layout, and any caller that also passes lastModified writes the timestamp 8 bytes deeper than the firmware expects.

Frame layout per the doc comment

The function is documented (line 695) as producing:

So the optional tail is 8 bytes of position + optional 4-byte timestamp — 12 bytes max.

What it actually emits

Two back-to-back blocks both write position data:

// Block 1 (lines 723-736) — runs unconditionally
if ((lat == null || lon == null) && lastModified != null) {
  writer.writeInt32LE(0);
  writer.writeInt32LE(0);
} else {
  final latitude = lat ?? 0.0;
  writer.writeInt32LE((latitude * 1e6).round());
  final longitude = lon ?? 0.0;
  writer.writeInt32LE((longitude * 1e6).round());
}

// Block 2 (lines 738-747) — runs whenever location OR lastModified is set
final hasLocation = lat != null && lon != null;
if (hasLocation || lastModified != null) {
  writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0);
  writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0);
  if (lastModified != null) {
    final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
    writer.writeUInt32LE(lastModifiedTimestamp);
  }
}

Block 1 runs unconditionally and emits 8 bytes. Block 2 then adds another 8 bytes whenever location or lastModified is set, plus the optional 4-byte timestamp.

For buildUpdateContactPathFrame(pk, path, n, lat: 49.1, lon: -123.1, lastModified: now):

Expected Actual
[lat x4][lon x4][ts x4] (12 bytes) [lat x4][lon x4][lat x4][lon x4][ts x4] (20 bytes)

The firmware parses the second lat field as the timestamp, getting wildly wrong values.

Reproduction

final frame = buildUpdateContactPathFrame(
  Uint8List(32), Uint8List(0), 0,
  lat: 49.0, lon: -123.0, lastModified: DateTime.now(),
);
// Base frame is 136 bytes (cmd + pubKey + type + flags + pathLen + path + name + timestamp)
expect(frame.length, 148); // 136 + 8 lat/lon + 4 timestamp
// Actual: 156 (= 136 + 16 lat/lon + 4 timestamp)

Proposed fix

Delete block 1; keep only block 2. Block 2 is structurally correct: skips the optional tail when neither location nor lastModified is set, zero-fills position slots when only lastModified is present, appends the optional timestamp when applicable.

I have a fix + 5-test suite (covering all four optional-tail combinations) ready in a downstream fork. Happy to open a PR if you'd like — let me know.

Context

Caught while adding wire-protocol tests to a downstream fork. There were no tests covering this function previously, which is why the bug wasn't surfaced earlier.

Affected user-facing path: MeshCoreConnector.importDiscoveredContact() at line 3085 is the only caller that passes all three optional args (lat, lon, lastModified). That means every imported discovered contact gets a corrupted timestamp on the radio — probably manifesting as wildly wrong "last seen" values in the UI.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions