diff --git a/lib/src/control.dart b/lib/src/control.dart index de8992e..adfbd6c 100644 --- a/lib/src/control.dart +++ b/lib/src/control.dart @@ -453,6 +453,22 @@ CmdResponse? parseCommandResponse(Uint8List inner, final op = inner[2]; final payload = Uint8List.sublistView(inner, 3); final dec = {}; + // A response body is preceded by the ECHOED REQUEST SEQ at inner[3] (the + // status follows it at inner[4]) — the same byte several decoders here + // already skip past to find their real first body byte. + // + // Surfaced so a caller can tell WHICH of its outstanding requests a reply + // belongs to. Without it every response for an opcode is indistinguishable, + // and a caller that awaits a specific read can be satisfied by an unrelated + // earlier request's reply — which matters for GET_CLOCK, where the app + // polls the RTC from several places at once and gates history offload on + // the answer. + // + // NOTE: that the strap echoes back the seq the PHONE sent is the layout this + // package has always assumed; it is not confirmed against a hardware capture + // here. Treat a mismatch as "not the reply I awaited", never as an error, + // and always keep a path that works when the correlation never matches. + if (inner.length >= 4) dec['req_seq'] = inner[3]; if (op == Cmd.getBatteryLevel && inner.length >= (profile.isGen5 ? 6 : 7)) { // Byte-verified: gen5 returns a DIRECT percent @ inner[5] (u8, e.g. // 0x2F=47%) — NOT deci-percent like gen4's u16 LE @[5:7]. Conflating the diff --git a/test/decode_guards_test.dart b/test/decode_guards_test.dart index 8865a59..d229fad 100644 --- a/test/decode_guards_test.dart +++ b/test/decode_guards_test.dart @@ -58,6 +58,8 @@ List _u32le(int v) => [v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >> 24) & 0xff]; void main() { + _reqSeqTests(); + // ── 1. R-R count / value bounds in the historical record decoder ────────── group('R24 R-R intervals are bounded (count + physiological value)', () { test('baseline: the unmodified record still decodes its 2 real beats', () { @@ -464,3 +466,29 @@ void main() { }); }); } + +/// The echoed request seq lets a caller tell WHICH outstanding request a reply +/// answers — see the note in [parseCommandResponse]. Regression: it used to be +/// dropped on the floor, so every reply for an opcode looked identical and a +/// caller awaiting one specific read could be satisfied by an earlier one's. +void _reqSeqTests() { + group('cmd_response echoes the request seq', () { + test('surfaces inner[3] as req_seq', () { + // `_cmdResponse` writes [0x24][seq][opcode] then the payload, so the + // payload's first byte IS inner[3] — the echoed seq, here 0x2A. + final r = parseCommandResponse(_cmdResponse(0x0B, [0x2A, 0x01]))!; + expect(r.decoded['req_seq'], 0x2A); + }); + + test('two replies to the same opcode are distinguishable', () { + final a = parseCommandResponse(_cmdResponse(0x0B, [7, 0x01]))!; + final b = parseCommandResponse(_cmdResponse(0x0B, [8, 0x01]))!; + expect(a.decoded['req_seq'], isNot(b.decoded['req_seq'])); + }); + + test('a truncated response carries no req_seq rather than a bogus one', () { + final r = parseCommandResponse(Uint8List.fromList([0x24, 0x11, 0x0B]))!; + expect(r.decoded.containsKey('req_seq'), isFalse); + }); + }); +}