From 1a266dbc1218f229d540fe97ba4d5a4d95a9ed1b Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:38:22 +0530 Subject: [PATCH] surface the echoed request seq on cmd_response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inner[3] is the seq the phone sent, which several decoders in here already step over to find their real first body byte — it just never made it out to callers. without it every reply for an opcode looks the same, so anything awaiting one specific read can be satisfied by an earlier request's reply. edge needs this for GET_CLOCK: the rtc gets polled from a few places at once and history offload is gated on the answer. the echo itself is the layout this package has always assumed and isn't confirmed against a capture, so callers should treat a mismatch as "not mine" and keep working when it never matches. --- lib/src/control.dart | 16 ++++++++++++++++ test/decode_guards_test.dart | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) 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); + }); + }); +}