fix: JRing history asks for today (#73), Connect button actually connects (#72) - #75
Open
foureight84 wants to merge 3 commits into
Open
foureight84 wants to merge 3 commits into
foureight84 wants to merge 3 commits into
Conversation
An SR08 synced steps and heart rate but never a single night of sleep, however long it was worn — the ring's own display and JYouPro both showed the night. Byte 1 of the `0x10` history query is a **day offset**, not a count of days. `IRemoteService.getDataByDay(int type, int day)` reaches `BluetoothLeService.a(int, int)`, which writes the type into `bArr[0]` (`1` → 16 = `0x10` activity/sleep, `2` → 22 = `0x16` heart rate) and the `day` argument straight into `bArr[1]` — one day per call, no count anywhere. Its caller loops: `DupMainActivity.onGetMultipleSportData` decrements `P_SYNC_HISTORY_DAY` on each day's sync-end and calls `getDataByDay(1, i6)` again, counting down to 0. Read as a count, `0x10/03` asked for the day before last and `0x10/01` for yesterday. **No pass ever requested today**, and `0x10` is the only source of sleep — there is no sleep-only request — so last night could not arrive at all. Steps and HR masked it: activity comes from the ring's automatic `0x03` push, and `makeHistoryMeasurementQueryCommand` already hardcoded `16 00`, i.e. offset 0. The one request that asked for today was the one that worked. So each day gets its own request: today always, plus the rest of the backfill window once per connection (the gate matters because `runStartup` is also the ~30-minute background sync). Today leads the window rather than trailing it as the vendor's countdown does — it is the day the user opened the app to see, and it is the shape the reporter hardware-validated on an SR08, where requesting offsets 0, 1 and 2 separately produced 27 `0x11` sleep entries after `0x10/00`. We stay fire-and-forget rather than reply-driven because nothing here decodes a per-day sync-end to chain from; AGENTS.md records that divergence and what would make the vendor's chain necessary. The count reading came in with a citation to Gadgetbridge's `triggerActivityReportByDays()` — an identifier that appears nowhere in the vendor SDK, only in our own files. iOS was never affected: its `makeHistoryQueryCommand()` is the fixed hex `1000…`, i.e. offset 0, and simply never backfilled. Also corrects two comments that cited the count semantics, and drops `syncWindowDays`' "must match makeHistoryQueryCommand's default" claim — it sizes the progress bar and nothing else, as AGENTS.md already warned after that same mistake deferred this fix once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tapping Connect on the Settings hero card did nothing after disconnecting the
ring from Settings — no spinner, no error, no attempt. "Reconnect last ring" on
the ring detail screen worked, which is what made it look like a UI-only fault.
Both buttons reconnect the same stored ring, but through different entry points.
`userDisconnect()` persists a stay-off flag so a manual Disconnect actually
sticks against every auto-reconnect path — app foreground, the watchdog,
`RingSyncWorker`, the coach runner — and `connectLastKnown()` honours it by
returning immediately. The detail screen calls `userConnect()`, which clears the
flag first; the hero card called `connectLastKnown()`, so it was suppressed by
the flag in precisely the situation the button exists for. The guard is right —
the caller was wrong.
The hero card and the pairing screen's "Reconnect last ring" now call
`userConnect()`: a user tapping Connect *is* the intent the flag was waiting for.
Every genuine auto-reconnect caller is untouched and still honours it.
`connectLastKnown()` gains a KDoc saying it is the auto path and that a button
must not call it, since its name reads exactly like what a Connect button wants.
Also fixes the same silent no-op one action along: the card rendered "Disconnect"
as an enabled button whose `when` branch fell into `else -> {}`. It is wired to
`userDisconnect()`, and the `when` is now exhaustive over the action enum, so a
future action can't be added without handling it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`docs/crp-r11-hardening-plan.md` and `docs/health-connect-integration.md` both said not to add a `Co-Authored-By` trailer, and 13 commits carry one anyway — because those are task-specific plan documents that nothing loads automatically. The rule was never in AGENTS.md, which is. GitHub reads the trailer as a second author and lists the address as a repository contributor, so an agent shows up in the contributors graph next to the maintainer. That is what prompted this. Existing commits are deliberately left alone: rewriting them would orphan the v2.8.0+51, v2.9.0+52-rc1, v2.9.0+53 and v2.9.1+54-rc1 tags and the published APKs hanging off them, to fix something cosmetic.
This was referenced Sep 15, 2026
|
RC1 hardware test follow-up for #73: v2.9.1+54-rc1 still shows only 45 minutes instead of the expected 6h30 on the SR08. The unredacted capture contains three 15-minute packets for today and one for yesterday; all three day requests plus HR are sent within 176 ms, with requests overlapping incoming sleep packets. Full decoded timeline and the working prototype's sequencing are documented here: #73 (comment) The working prototype validated explicit offsets with waiting between transfers, not the queued-together sequence in this PR. Please address the sequencing before treating #73 as resolved. The capture supports incomplete retrieval, although it does not isolate which subsequent command interrupts the ring. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #73, closes #72.
Two unrelated bugs that share a shape: something the app asks for is silently answered by nobody, and nothing on screen says so.
1353 tests, 0 failures. No schema changes. Neither is hardware-tested here — #73 has the reporter's own prototype validation on an SR08, #72 is verifiable on any ring in about ten seconds.
#73 — the JRing history request asked for the wrong day
@srinikandala's report is the good kind: it names the bytes, cites the vendor app, and comes with a prototype that was tried on hardware. It's right, and the vendor SDK settles it.
Byte 1 of
0x10is a day offset, not a count of days.IRemoteService.getDataByDay(int type, int day)reachesBluetoothLeService.a(int, int), which writes the type intobArr[0]—1→ 16 (0x10, activity+sleep),2→ 22 (0x16, heart rate) — and then thedayargument straight intobArr[1]. One day per call; there is no count anywhere in the path. The caller loops:DupMainActivity.onGetMultipleSportDatadecrementsP_SYNC_HISTORY_DAYon each day's sync-end and callsgetDataByDay(1, i6)again, counting down to 0.Read as a count, our
0x10/03asked for the day before last and0x10/01for yesterday. No pass ever requested today — and0x10is the only source of sleep, since there's no sleep-only request — so last night's sleep could not arrive however many times the app synced.Why steps and HR hid it. Neither depends on this request for today: activity arrives on the ring's automatic
0x03push, andmakeHistoryMeasurementQueryCommandalready hardcoded16 00— offset 0. The one request in the file that asked for today was the one that worked, which is the clue that was sitting in the source the whole time.The fix. Each day gets its own request: today always, plus the rest of the backfill window once per connection (that gate matters —
runStartupis also the ~30-minute background sync, and each extra day is ~96 activity packets on top of the night).Today leads the window, where the vendor's countdown leaves it until last. It's the day the user opened the app to see, and it's the shape the reporter validated: requesting offsets 0, 1 and 2 separately produced 27
0x11sleep entries after0x10/00, and sleep appeared. We also stay fire-and-forget rather than reply-driven, because nothing here decodes a per-day sync-end to chain from. Both divergences are deliberate and recorded inAGENTS.mdalong with what would make the vendor's chain necessary — a ring seen truncating a day's stream when the next request lands.Where the wrong reading came from. The KDoc credited Gadgetbridge's
triggerActivityReportByDays(). That identifier appears nowhere indecompiled-jring-offical/— only in our own files. A citation to another project's helper isn't vendor evidence, and the count semantics arrived with it. iOS was never affected: itsmakeHistoryQueryCommand()is the fixed hex1000…, i.e. offset 0 — correct for today, and it simply never backfilled.The reporter's timing observation (JYouPro waits for the
0x10/0x11stream to go quiet before requesting HR; we sent HR ~30 ms later) is not changed here, and their own timing-only prototype didn't fix sleep — so it isn't the bug. Worth revisiting only if a capture shows a truncated stream.#72 — Connect on the Settings card did nothing
@Albabit found this while testing #70: disconnect from Settings, tap Connect on the ring card, and nothing happens at all. No spinner, no error, no attempt. "Reconnect last ring" on the detail screen works, which made it look like a UI-only fault.
Both buttons reconnect the same stored ring — through different entry points.
userDisconnect()persists a stay-off flag so a manual Disconnect actually sticks against every auto-reconnect path (app foreground, the watchdog,RingSyncWorker, the coach runner), andconnectLastKnown()honours it by returning immediately. The detail screen callsuserConnect(), which clears the flag first. The hero card calledconnectLastKnown()— so it was suppressed by the flag in exactly the situation the button exists for.The guard is right; the caller was wrong. The hero card and the pairing screen's "Reconnect last ring" now call
userConnect()— a user tapping Connect is the intent the flag was waiting for. Every genuine auto-reconnect caller is untouched and still honours it.connectLastKnown()gains a KDoc saying it's the auto path and that a button must not call it, because its name reads exactly like what a Connect button wants. I considered renaming it, but its six callers are background workers and the coach runner — files with no business in this diff.One more silent no-op, one action along. The same card rendered "Disconnect" as an enabled button whose
whenbranch fell intoelse -> {}. It's wired touserDisconnect()now, and thewhenis exhaustive over the action enum so a future action can't be added without handling it. (This is presumably why the reporter disconnected from the detail screen rather than the card.)Testing and risk
./gradlew testDebugUnitTest— 1353 tests, 0 failures. The JRing history tests were rewritten around offsets: that every pass asks for today, that the deep window is once per connection and today-only after it, that today leads, and that offsets survive the 0..27 coerce. The discriminating one is "always asks for today" — it's the assertion that would have caught this.Behaviour change worth knowing: the first pass of a JRing connection now sends three
0x10requests instead of one, so first-connection sync pulls roughly three days of activity where it previously pulled one (badly). Later passes are unchanged in volume — one request, now for the right day.Not hardware-tested here. #73's shape is the reporter's validated prototype but this is our implementation of it; #72 needs a ring, a Disconnect and one tap.
🤖 Generated with Claude Code