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 @@ -868,10 +868,18 @@ CmdResponse? parseCommandResponse(Uint8List inner,
// body[2:6] epoch u32 LE body[6:8] subseconds u16 LE
// — which confirms the epoch offset used here, and adds the active flag.
// The response carries no alarm ID; the requested ID selects it.
//
// Status-gated on gen5 ONLY, same convention as getClock/getDataRange/
// getBatteryPackInfo above: this opcode is sent to both profiles (see
// commands.dart), gen5's status byte is confirmed so a failure/deferred
// reply's stale body (leftover bytes from a prior successful read) isn't
// reported as the strap's current alarm; gen4's status byte is
// unconfirmed so it stays ungated, same reasoning as those siblings.
final statusOk = !profile.isGen5 || status == 1;
final form = payload.length >= 3 ? payload[2] : -1;
if (form == 0x01 && payload.length >= 7) {
if (statusOk && form == 0x01 && payload.length >= 7) {
dec['alarm_epoch'] = u32(payload, 3);
} else if (form == 0x04 && payload.length >= 8) {
} else if (statusOk && form == 0x04 && payload.length >= 8) {
dec['alarm_epoch'] = u32(payload, 4);
// "exactly 1 means active" — anything else is not an armed alarm, and is
// reported as inactive rather than guessed at.
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 @@ -310,6 +310,43 @@ void main() {
expect(get4(2)['alarm_active'], isFalse);
});

test(
'gen5: a non-success outer status emits neither alarm_epoch nor '
'alarm_active', () {
// Same convention as getClock/getDataRange: a failure/deferred reply's
// body bytes are stale leftovers from a prior successful read.
final body = [0x04, 1, ...le32(1786000000), ...le16(0)];
for (final status in [0, 2, 3]) {
final r = parseCommandResponse(
cmdResponse(Cmd.getAlarmTime, body, status: status),
profile: BandProfile.gen5)!;
expect(r.decoded.containsKey('alarm_epoch'), isFalse,
reason: 'status=$status');
expect(r.decoded.containsKey('alarm_active'), isFalse,
reason: 'status=$status');
}
});

test('gen5: a success outer status still decodes normally', () {
final body = [0x04, 1, ...le32(1786000000), ...le16(0)];
final r = parseCommandResponse(
cmdResponse(Cmd.getAlarmTime, body, status: 1),
profile: BandProfile.gen5)!;
expect(r.decoded['alarm_epoch'], 1786000000);
expect(r.decoded['alarm_active'], isTrue);
});

test('gen4 is left ungated on outer status, unlike gen5', () {
// Matches getClock/getDataRange's gen4 policy: the status byte's
// semantics are unconfirmed on gen4, so gating it would silently drop
// valid alarm reads rather than risk a stale one.
final body = [0x04, 1, ...le32(1786000000), ...le16(0)];
final r = parseCommandResponse(
cmdResponse(Cmd.getAlarmTime, body, status: 0))!;
expect(r.decoded['alarm_epoch'], 1786000000);
expect(r.decoded['alarm_active'], isTrue);
});

test('gen5 GET_CUSTOM_ADVERTISING_NAME (0x8D) decodes like gen4 0x4C', () {
// Reply body: revision, status, length, then the ASCII name — the same
// shape at the same offsets on both generations.
Expand Down
Loading