From 20a81ff7698a7fde34a11320ef550683c272e388 Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Tue, 29 Sep 2026 05:32:23 +0000 Subject: [PATCH 1/2] oura: decode the rtc_beacon wall-clock anchor (0x85) Co-authored-by: BucciMobile --- lib/src/oura.dart | 36 ++++++++++++++++++++++++++++++++++++ test/oura_test.dart | 16 ++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/src/oura.dart b/lib/src/oura.dart index 0b7a7a8..882f05e 100644 --- a/lib/src/oura.dart +++ b/lib/src/oura.dart @@ -99,6 +99,10 @@ const int kOuraEvtTemp = 0x46; /// A single skin-temperature reading. const int kOuraEvtTempPeriod = 0x69; +/// The RTC beacon, a wall-clock anchor the ring emits on its own. See +/// [decodeRtcBeacon]. +const int kOuraEvtRtcBeacon = 0x85; + /// Firmware diagnostics. Subtype-multiplexed; see [decodeDebugData]. const int kOuraEvtDebugData = 0x61; @@ -127,6 +131,38 @@ int? decodeTimeSync(OuraEvent e) { return (v >= 1700000000 && v <= 4100000000) ? v : null; } +/// One `rtc_beacon` (`0x85`) body: a Unix second plus a trailer. +class OuraRtcBeacon { + /// Unix seconds, 1-second granular. + final int unixSeconds; + + /// The frame's trailer, two bytes at body offset 8. + final int trailer; + + const OuraRtcBeacon(this.unixSeconds, this.trailer); +} + +/// Decode one `rtc_beacon` (`0x85`) body, or null when [e] is not one. +/// +/// The layout is ported code for code from open_oura's +/// `decode_rtc_beacon` (crates/oura-protocol/src/events.rs:356), the only +/// independent oracle this layout has: `u32` LE Unix seconds at offset 0, +/// reserved bytes in between, and a `u16` LE trailer at offset 8. Bodies +/// shorter than the 10 bytes the trailer needs are refused rather than read +/// half-way. +/// +/// UNLIKE [decodeTimeSync] this is the ring speaking unprompted, and the value +/// needs no plausibility window of its own to be useful: it is a beacon a +/// caller pairs with the envelope decisecond it arrived on, and a caller that +/// wants a date does its own refusing. It is also 1-second granular, where the +/// time-sync event only proves the RTC was set to the second it carries. +OuraRtcBeacon? decodeRtcBeacon(OuraEvent e) { + if (e.tag != kOuraEvtRtcBeacon || e.body.length < 10) return null; + final d = e.body.buffer.asByteData(e.body.offsetInBytes); + return OuraRtcBeacon( + d.getUint32(0, Endian.little), d.getUint16(8, Endian.little)); +} + /// Skin temperature in degrees Celsius, one entry per probe. /// /// The wire carries signed 16-bit little-endian CENTI-degrees. Anything outside diff --git a/test/oura_test.dart b/test/oura_test.dart index 010b6cb..1e3230e 100644 --- a/test/oura_test.dart +++ b/test/oura_test.dart @@ -243,6 +243,22 @@ void main() { parseOuraFrame(_hex('4208') + _hex('01000000') + _hex('00000000'))!)!; expect(decodeTimeSync(unset), isNull); }); + test('an rtc beacon is a unix second and a trailer, little-endian', () { + // Port of open_oura's `decode_rtc_beacon` vector shape: u32 LE Unix + // seconds, reserved bytes, u16 LE trailer at offset 8. + final ok = parseOuraEvent(parseOuraFrame( + _hex('850e') + _hex('01000000') + _hex('4fd2376a00000000e803'))!)!; + expect(decodeRtcBeacon(ok)!.unixSeconds, 1782043215); + expect(decodeRtcBeacon(ok)!.trailer, 0x03e8); + // A body too short for the trailer is not a beacon. + final short = parseOuraEvent(parseOuraFrame( + _hex('8509') + _hex('01000000') + _hex('4fd2376a00000000'))!)!; + expect(decodeRtcBeacon(short), isNull); + // A different tag is not a beacon either. + final other = parseOuraEvent(parseOuraFrame( + _hex('420e') + _hex('01000000') + _hex('4fd2376a00000000e803'))!)!; + expect(decodeRtcBeacon(other), isNull); + }); }); group('outbound frames', () { From 7d1600eef05f9db6fa352c6199998d66e734d3af Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:08:07 +0530 Subject: [PATCH 2/2] oura: rtc beacon gets the same date window as time sync, drop the unknown trailer --- lib/src/oura.dart | 39 +++++++++++---------------------------- test/oura_test.dart | 13 +++++++------ 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/lib/src/oura.dart b/lib/src/oura.dart index 882f05e..249dd14 100644 --- a/lib/src/oura.dart +++ b/lib/src/oura.dart @@ -131,36 +131,19 @@ int? decodeTimeSync(OuraEvent e) { return (v >= 1700000000 && v <= 4100000000) ? v : null; } -/// One `rtc_beacon` (`0x85`) body: a Unix second plus a trailer. -class OuraRtcBeacon { - /// Unix seconds, 1-second granular. - final int unixSeconds; - - /// The frame's trailer, two bytes at body offset 8. - final int trailer; - - const OuraRtcBeacon(this.unixSeconds, this.trailer); -} - -/// Decode one `rtc_beacon` (`0x85`) body, or null when [e] is not one. -/// -/// The layout is ported code for code from open_oura's -/// `decode_rtc_beacon` (crates/oura-protocol/src/events.rs:356), the only -/// independent oracle this layout has: `u32` LE Unix seconds at offset 0, -/// reserved bytes in between, and a `u16` LE trailer at offset 8. Bodies -/// shorter than the 10 bytes the trailer needs are refused rather than read -/// half-way. +/// Unix seconds from an `rtc_beacon` (`0x85`) body, or null when [e] is not +/// one or the value is not a date. /// -/// UNLIKE [decodeTimeSync] this is the ring speaking unprompted, and the value -/// needs no plausibility window of its own to be useful: it is a beacon a -/// caller pairs with the envelope decisecond it arrived on, and a caller that -/// wants a date does its own refusing. It is also 1-second granular, where the -/// time-sync event only proves the RTC was set to the second it carries. -OuraRtcBeacon? decodeRtcBeacon(OuraEvent e) { +/// Layout: `u32` LE Unix seconds at offset 0, then reserved bytes and a `u16` +/// at offset 8 whose meaning is unknown. Bodies under 10 bytes are refused. +/// Same date window as [decodeTimeSync], so an unset RTC never becomes an +/// anchor. +int? decodeRtcBeacon(OuraEvent e) { if (e.tag != kOuraEvtRtcBeacon || e.body.length < 10) return null; - final d = e.body.buffer.asByteData(e.body.offsetInBytes); - return OuraRtcBeacon( - d.getUint32(0, Endian.little), d.getUint16(8, Endian.little)); + final v = e.body.buffer + .asByteData(e.body.offsetInBytes) + .getUint32(0, Endian.little); + return (v >= 1700000000 && v <= 4100000000) ? v : null; } /// Skin temperature in degrees Celsius, one entry per probe. diff --git a/test/oura_test.dart b/test/oura_test.dart index 1e3230e..aee42b0 100644 --- a/test/oura_test.dart +++ b/test/oura_test.dart @@ -243,14 +243,11 @@ void main() { parseOuraFrame(_hex('4208') + _hex('01000000') + _hex('00000000'))!)!; expect(decodeTimeSync(unset), isNull); }); - test('an rtc beacon is a unix second and a trailer, little-endian', () { - // Port of open_oura's `decode_rtc_beacon` vector shape: u32 LE Unix - // seconds, reserved bytes, u16 LE trailer at offset 8. + test('an rtc beacon is a little-endian unix second', () { final ok = parseOuraEvent(parseOuraFrame( _hex('850e') + _hex('01000000') + _hex('4fd2376a00000000e803'))!)!; - expect(decodeRtcBeacon(ok)!.unixSeconds, 1782043215); - expect(decodeRtcBeacon(ok)!.trailer, 0x03e8); - // A body too short for the trailer is not a beacon. + expect(decodeRtcBeacon(ok), 1782043215); + // A body too short for the full layout is not a beacon. final short = parseOuraEvent(parseOuraFrame( _hex('8509') + _hex('01000000') + _hex('4fd2376a00000000'))!)!; expect(decodeRtcBeacon(short), isNull); @@ -258,6 +255,10 @@ void main() { final other = parseOuraEvent(parseOuraFrame( _hex('420e') + _hex('01000000') + _hex('4fd2376a00000000e803'))!)!; expect(decodeRtcBeacon(other), isNull); + // An unset RTC is not an anchor. + final unset = parseOuraEvent(parseOuraFrame( + _hex('850e') + _hex('01000000') + _hex('0100000000000000e803'))!)!; + expect(decodeRtcBeacon(unset), isNull); }); });