Skip to content

protocol: decode ring_start (0x41) and user_information (0x5c) - #12

Open
nsandoval wants to merge 1 commit into
Th0rgal:mainfrom
nsandoval:decode-ring-start-and-user-information
Open

protocol: decode ring_start (0x41) and user_information (0x5c)#12
nsandoval wants to merge 1 commit into
Th0rgal:mainfrom
nsandoval:decode-ring-start-and-user-information

Conversation

@nsandoval

@nsandoval nsandoval commented Aug 22, 2026

Copy link
Copy Markdown

These were the only two event types still reaching the store with a name but no body decoder, so a full sync left them raw. With both landed, a sync of my Ring 4 decodes 19,117 / 19,117 events.

Tested on a Ring 4 Horizon (ORE_06, firmware 2.12.3) paired with a client-generated key via oura pair.

Checked against the open branches first: this does not overlap #7, which leaves 0x41 and 0x5c untouched.

ring_start (0x41)

A boot record. Layout:

04 00 00 00 | 32 | 02 0C 03 | 01 00 01 | 02 01 00
 u32 reason   ?    fw 2.12.3  bl 1.0.1   api 2.1.0

Recovered by correlating a captured body against the DeviceInfo response of the same ring: bytes 5..14 are byte for byte the firmware, bootloader and API triplets that command reports. The leading u32 and the byte at offset 4 are not identified — one sample only — so they are surfaced verbatim (reason, unknown_byte_4) rather than guessed at.

user_information (0x5c)

Four bytes of anthropometric profile: [age_years][weight_kg][sex][height_cm].

Field meanings here are inferred, not recovered from libringeventparser.so, so the decoder emits "_status": "inferred". Flagging that explicitly since it differs from how every other decoder in the crate was obtained. The basis:

  • ecore's own metabolic functions take exactly these inputs — vo2max_jackson(age, female, weight_kg) and bmr_schofield(age, sex, weight_kg), both already ported in oura-analysis::ported::metabolic.
  • The sleep-score limits are initialised "from age byte" (init_limits_v2 @ 0x1f5b20, per docs/algorithms/README.md).
  • On the captured sample the sex byte is 2 — neither male (0) nor female (1), but the value bmr_schofield documents as "anything else", the unset path that averages both sexes. That ring had just been factory reset, so 40 y / 75 kg / 176 cm read as firmware defaults rather than a configured profile.

height_cm is the least certain field and is called out as such: no ported algorithm consumes it, since steps_to_meters uses a fixed 0.762 m stride.

Why this matters beyond completeness

If the reading is right, a ring that was re-keyed rather than onboarded through the app runs its metabolic path on defaults. For my body composition that is a 21% low BMR and a 25% systematic underestimate of active calories, since those derive from MET x weight. Anything downstream of oura-analysis::ported::metabolic inherits that error silently.

Possibly worth a follow-up

tools/oura_protocol.py describes the 0x20 setters as the "unused gender/height/weight field". If the reading above is right they are not unused, just unset. I left that wording alone because I cannot yet prove a write persists: the ring ACKs set_user_gender_empty with 21020200 (type=2, result=0), but there is no read command for the profile, and the only user_information emission I have landed at exactly 00:00, which looks scheduled rather than change-driven. Happy to follow up once a round trip confirms it. This may also be a partial answer to #8.

Testing

  • 4 new tests using the captured bytes, including short-body rejection and the sex-code mapping.
  • cargo test -p oura-protocol → 37 passed.
  • cargo clippy -p oura-protocol → clean.
  • cargo fmt --check → the new code adds no diffs of its own.
  • Verified end to end with oura redecode, which backfilled both events from bodies stored before the decoders existed.

These were the only two event types still reaching the store with a name
but no body decoder, so a full sync left them raw.

ring_start is a boot record: a u32 reason word, one unidentified byte,
then the firmware, bootloader and API version triplets — byte for byte
the values DeviceInfo reports for the same ring. Recovered by correlating
a captured body against that command's response on a Ring 4 (ORE_06)
right after a factory reset.

user_information is four bytes of anthropometric profile. Its field
meanings are inferred rather than recovered from libringeventparser.so,
so the decoder emits "_status": "inferred". The basis is that ecore's own
metabolic functions take exactly these inputs — vo2max_jackson(age,
female, weight_kg) and bmr_schofield(age, sex, weight_kg), both ported in
oura-analysis::ported::metabolic — and the sleep-score limits are
initialised "from age byte". On the captured sample the sex byte is 2:
neither male (0) nor female (1), but the value bmr_schofield documents as
"anything else", the unset path that averages both sexes. That ring had
just been factory reset, so the values read as firmware defaults (40 y,
75 kg, 176 cm) rather than a configured profile. height_cm is flagged as
the least certain field, since no ported algorithm consumes it —
steps_to_meters uses a fixed 0.762 m stride.

Both layouts are covered by tests using the captured bytes, and the crate
README notes their provenance, which is not the usual Ghidra port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nsandoval
nsandoval force-pushed the decode-ring-start-and-user-information branch from 689091b to d41c4e7 Compare August 22, 2026 12:46
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