Skip to content

Android: name all 58 strap events, reviving the dropped-RTC diagnostic (#791) - #793

Merged
ryanbr merged 1 commit into
mainfrom
fix/android-event-catalogue-791
Jul 25, 2026
Merged

ryanbr merged 1 commit into
mainfrom
fix/android-event-catalogue-791

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

From reviewing #791. The reporter listed an "uncatalogued event 0x44=68" from an Android capture, which
prompted a look at what Android actually names.

The gap

The shared schema (whoop_protocol.json, which iOS reads at runtime) names 58 strap events. Android's
EventNumber enum listed 13. Everything else reached an Android strap log as a bare 0x44(68).

The cosmetic consequence is that an Android bug report cannot say what the strap is reporting. The events
that would have explained #791's other two findings all arrived anonymous:

Event Bears on
63 EXTENDED_BATTERY_INFORMATION the battery hunt — an event-driven source the report never tested
13 RTC_LOST, 16 SET_RTC finding 2, scrambled RTC and SET_CLOCK not sticking
15 BOOT, 30 BOOT_REPORT, 43 BLE_SYSTEM_RESET finding 3, the brownout/reboot hypothesis
1 ERROR, 2 CONSOLE_OUTPUT strap-side errors, visible only as numbers

The real half: a dead diagnostic

DroppedRtcEvent.isRtcStateKind matches the event label by prefix (kind.startsWith("RTC_LOST")), and
the label is built from this enum as "NAME(raw)". While 13/15/16/30 were unnamed their labels were
0x0D(13) and friends, which match nothing — so the #324 dropped-RTC diagnostic was dead code on
Android
, though it mirrors Swift exactly (Streams.swift:317) and iOS has captured it since #324
(HistoricalTimestampGateTests pins the string "RTC_LOST(13)").

That is the diagnostic which explains a future-dated strap. #791 reports the strap claiming 2030-01-28
with nothing recording why — on Android there could not have been.

droppedRtcEvents is documented "Diag only" and flows to the strap log via Backfiller, so this restores a
diagnostic rather than altering stored data or analytics.

Scope

Names only. Event payload decoding is untouched — it stays family-gated and deliberately conservative,
because several payloads have no on-device 5.0 ground truth and are left raw rather than ported from 4.0 on
faith (Interpreter.swift:786). A name here does not imply a decoder. A number absent from the schema still
falls through to the hex label and is never given an invented name: event 68, observed on real hardware
in #791, is unknown to both platforms and stays unknown.

No iOS change needed — iOS resolves names from the shared JSON at runtime and already had all 58.

Verification

Entries were generated from whoop_protocol.json, not transcribed. Verified mechanically:

  • Android enum ≡ schema (58/58), no duplicate raw values, all valid Kotlin identifiers, and all 13
    pre-existing names already agreed with the schema so nothing is renamed
  • Audited every Android EventNumber usage — one label site and two == BATTERY_LEVEL comparisons, no
    null-as-invalid checks, so UNDEFINED(0) is safe to include for exact label parity
  • Checked every Android test for a pinned hex label of a now-named event: only FramingTest's event 29,
    updated here
  • No wildcard import of the enum anywhere, so the new entries cannot shadow an identifier
  • org.json confirmed as a testImplementation dependency; the locator resolves from both the module dir
    and the repo root
  • Tools/i18n_audit.py --ci origin/main → exit 0

Corrected during review

The first draft of the lockstep test pinned the enum against a hand-transcribed copy of the catalogue.
That would have caught a deletion or a renumber but sailed straight through a recurrence of this very bug —
the schema growing while Android stands still, which is exactly how the gap arose. The test now reads the
schema
, following DecoderOracleTest's existing convention for reaching the Swift tree: org.json, the
two-candidate user.dir locator, and an Assume-skip when that tree is absent. The targeted assertions for
the #791 events and the revived RTC diagnostic never skip, so they hold either way.

Worth being precise about the guard's reach: the lockstep check is a drift guard for a full checkout, not a
hard gate in every context, because it skips when the Swift tree is missing. That is the repo's existing
convention rather than a new compromise.

Additional checks, no defects found

Recorded so a reviewer need not re-derive them:

  • Ordinal shift. New entries were inserted before BATTERY_LEVEL, so every ordinal moved. Nothing
    persists this enum by ordinal, name, valueOf, or a Room TypeConverter — the only .ordinal uses in
    the app belong to unrelated enums, and the full usage audit found one label site plus two
    == BATTERY_LEVEL comparisons. Safe.
  • The shared decoder oracle. decoder_oracle.json is asserted byte-identical across platforms and pins
    expected decodes for both. It contains no event labels at all (type-47 HISTORICAL_DATA frames, not EVENT
    frames), and the two copies remain identical. Unaffected.
  • Stale expectations. Swept every file under src/test, src/androidTest and src/main/assets for a
    pinned hex label of a now-named event. Only FramingTest's event 29, already updated.
  • The newly-live path under load. Android's dropped-RTC logging loop is an exact twin of
    Strand/Collect/Backfiller.swift — same message, and uncapped on both sides by the same documented
    rationale that these events are sparse rather than per-record. Reviving it introduces no divergence.
  • Style. No ktlint, detekt, or editorconfig gate in the repo; longest line here is 110 against 129
    elsewhere in com.noop.protocol.

The one thing still unverified is compilation. The specific Kotlin surface used — JSONObject.keys()
.asSequence(), Assume.assumeTrue(String, Boolean), Set<Int> difference, EventNumber.entries — was
reasoned through rather than compiled, which is not the same thing.

Not run: the JUnit suite. This machine has no JDK, so the assertions were verified by mechanically
simulating the same data and parsing against the real schema file rather than by execution. A real
gradlew test before merge is worth it — android.yml is disabled, so CI will not do it either.

#791)

The Android `EventNumber` enum listed 13 of the 58 events the shared schema names,
so every other event reached an Android strap log as a bare `0x44(68)` while iOS
named it. #791 was filed partly on the strength of an "uncatalogued" event that
iOS already knew, and the events that would have explained that report's other
two findings — RTC_LOST, BOOT, BLE_SYSTEM_RESET — all arrived anonymous.

The cosmetic half is that an Android bug report cannot say what the strap is
reporting. The real half is a behaviour bug: `DroppedRtcEvent.isRtcStateKind`
matches the event LABEL by prefix, and the label is built from this enum. With 13
and 15/16/30 unnamed their labels were `0x0D(13)` and friends, which match
nothing, so the #324 dropped-RTC diagnostic was dead code on Android while its
Swift twin has captured it since #324. That is the diagnostic that explains a
future-dated strap, which is exactly the symptom in #791 — the reporter saw the
strap claim 2030-01-28 and no diagnostic recorded why.

Names only, generated from whoop_protocol.json rather than transcribed. Event
PAYLOAD decoding is untouched: it stays family-gated and deliberately
conservative, because several of these payloads have no on-device 5.0 ground
truth and are left raw rather than ported from 4.0 on faith. A name here does not
imply a decoder, and a number absent from the schema still falls through to the
hex label rather than being given an invented name — event 68 observed on real
hardware in #791 is unknown to both platforms and stays unknown.

No iOS change: iOS resolves names from the shared JSON at runtime and already had
all 58.

The lockstep test READS the schema rather than restating it. A first draft pinned
the enum against a hand-transcribed copy of the catalogue, which would have caught
a deletion or a renumber but sailed straight through a recurrence of this very bug
— the schema growing while Android stands still. It follows DecoderOracleTest's
existing convention for reaching the Swift tree: org.json (a real testImplementation
dependency, since android.jar ships throwing stubs), the two-candidate user.dir
locator, and an Assume-skip when that tree is absent. The targeted assertions for
the #791 events and the revived RTC diagnostic never skip.

FramingTest's event-29 expectation moves from `0x1D(29)` to
`STRAP_CONDITION_REPORT(29)`; its comment called 29 uncatalogued, which the shared
schema contradicts. Its payload assertion is left alone and is what now pins the
name-without-decoder distinction.
@ryanbr
ryanbr force-pushed the fix/android-event-catalogue-791 branch from 42a3bd4 to a186e2f Compare July 25, 2026 04:24
@ryanbr
ryanbr merged commit 071b581 into main Jul 25, 2026
1 check passed
ryanbr added a commit that referenced this pull request Jul 25, 2026
MARKETING_VERSION/versionName move together per the versioning rule; build numbers
increment independently (207->208 Apple, 298->299 Android).

The staging workflow reads MARKETING_VERSION for its title and asset names, so main
has to carry 9.1.1 for the rolling testing-latest release to be labelled as such —
the previous 9.1.1 staging build was cut from a commit that never landed on main.

This is also the first compile of the app-target changes merged today (#783/#784/#785,
#787, #788, #793, #795, #519/#798, #523, #778), which CI does not cover: app-build.yml
and android.yml are disabled and only the Swift packages and the i18n gate run on PRs.
@ryanbr
ryanbr deleted the fix/android-event-catalogue-791 branch August 2, 2026 07:41
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
ryanbr#791) (ryanbr#793)

The Android EventNumber enum listed 13 of the 58 events the shared schema names, so
every other event reached an Android strap log as a bare 0x44(68) while iOS named
it. ryanbr#791 was filed partly on the strength of an "uncatalogued" event iOS already
knew, and the events that would have explained that report's other two findings —
RTC_LOST, BOOT, BLE_SYSTEM_RESET — all arrived anonymous.

The cosmetic half is that an Android bug report cannot say what the strap is
reporting. The real half is a behaviour bug: DroppedRtcEvent.isRtcStateKind matches
the event LABEL by prefix, and the label is built from this enum. With 13 and
15/16/30 unnamed their labels matched nothing, so the ryanbr#324 dropped-RTC diagnostic
was dead code on Android while its Swift twin has captured it since ryanbr#324. That is
the diagnostic that explains a future-dated strap — the symptom in ryanbr#791, where the
strap claimed 2030-01-28 and nothing recorded why.

Names only. Event payload decoding is untouched: family-gated and deliberately
conservative, because several payloads have no on-device 5.0 ground truth. A name
does not imply a decoder, and a number absent from the schema still falls through to
the hex label rather than being invented — event 68 stays unknown.

No iOS change: iOS resolves names from the shared JSON and already had all 58.

Does NOT resolve ryanbr#791. Battery percent is still unavailable on that firmware: nothing
consumes EXTENDED_BATTERY_INFORMATION into batteryPct on either platform, so naming
event 63 is log visibility only. This makes the evidence readable, nothing more.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
MARKETING_VERSION/versionName move together per the versioning rule; build numbers
increment independently (207->208 Apple, 298->299 Android).

The staging workflow reads MARKETING_VERSION for its title and asset names, so main
has to carry 9.1.1 for the rolling testing-latest release to be labelled as such —
the previous 9.1.1 staging build was cut from a commit that never landed on main.

This is also the first compile of the app-target changes merged today (ryanbr#783/ryanbr#784/ryanbr#785,
ryanbr#787, ryanbr#788, ryanbr#793, ryanbr#795, ryanbr#519/ryanbr#798, ryanbr#523, ryanbr#778), which CI does not cover: app-build.yml
and android.yml are disabled and only the Swift packages and the i18n gate run on PRs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant