Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions lib/src/control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,15 @@ CmdResponse? parseCommandResponse(Uint8List inner,
// (mod 4), so the grid could never land on either and range_oldest /
// range_newest were never emitted at all.
final revOk = payload.length > 2 && payload[2] == 1;
if (revOk && payload.length >= 63) {
// Status-gated like the clock read above: a failure reply leaves the body
// unpopulated, so a stale revision byte and stale-but-plausible page/
// backlog numbers from a prior successful read would otherwise be
// reported as current. Same shared-opcode shape as getClock/getClockGen5
// (this opcode is used by both profiles — see commands.dart), so gen4
// is deliberately left ungated for the same unconfirmed-status-byte
// reason documented there.
final statusOk = !profile.isGen5 || status == 1;
if (statusOk && revOk && payload.length >= 63) {
final oldest = u32(payload, 35);
final newest = u32(payload, 59);
if (_plausibleUnix(oldest) && _plausibleUnix(newest) && oldest <= newest) {
Expand All @@ -965,7 +973,7 @@ CmdResponse? parseCommandResponse(Uint8List inner,
// strap's own view of its ring buffer, which is what separates a stalled
// offload from an idle one. Degrades safely: implausible values emit
// nothing at all.
if (revOk && payload.length >= 35) {
if (statusOk && revOk && payload.length >= 35) {
final writePage = u32(payload, 11);
final capacity = u32(payload, 23); // TotalPages
if (capacity > 0 && writePage <= capacity) {
Expand Down
37 changes: 37 additions & 0 deletions test/control_plane_offsets_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,43 @@ void main() {
dataRangeBody(writePage: 200000, totalPages: 131072)))!;
expect(r.decoded.containsKey('pages_behind'), isFalse);
});

test('gen5: a non-success outer status emits neither range nor backlog',
() {
// A failure reply's body bytes are stale leftovers from a prior
// successful read; the revision byte alone (payload[2]==1) is not
// enough to trust them.
for (final status in [0, 2, 3]) {
final r = parseCommandResponse(
cmdResponse(Cmd.getDataRange, dataRangeBody(), status: status),
profile: BandProfile.gen5)!;
expect(r.decoded.containsKey('range_oldest'), isFalse,
reason: 'status=$status');
expect(r.decoded.containsKey('range_newest'), isFalse,
reason: 'status=$status');
expect(r.decoded.containsKey('pages_behind'), isFalse,
reason: 'status=$status');
}
});

test('gen5: a success outer status still decodes normally', () {
final r = parseCommandResponse(
cmdResponse(Cmd.getDataRange, dataRangeBody(), status: 1),
profile: BandProfile.gen5)!;
expect(r.decoded['range_oldest'], 1780000000);
expect(r.decoded['range_newest'], 1786000000);
expect((r.decoded['pages_behind'] as Map)['written'], 30);
});

test('gen4 is left ungated on outer status, unlike gen5', () {
// Matches getClock's gen4 policy a few groups up: the status byte's
// semantics are unconfirmed on gen4, so gating it would silently drop
// range/backlog emission rather than risk a stale read.
final r = parseCommandResponse(
cmdResponse(Cmd.getDataRange, dataRangeBody(), status: 0))!;
expect(r.decoded['range_oldest'], 1780000000);
expect(r.decoded['range_newest'], 1786000000);
});
});

// The alarm/haptics status byte: the SET and RUN
Expand Down
Loading