Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 120 additions & 8 deletions Tools/parity_dispositions.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,6 @@
"platform": "kotlin",
"rationale": "Android port of the Swift Test Centre feature-mode experiment (#2105, OURA_PROTOCOL.md s7.5) for this Android-first fork. The Swift twins already exist in Packages/OuraProtocol; the Kotlin builders keep the same bytes and are reachable only from the Android Test Centre behind a confirmation, never automatically."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/EventTags.kt::SPO2_R_PI_UNDECODED",
"identity_sha256": "a3712c91c37eb0465f18ce767a15017a29039d9de56adbc252a10e3b5ac4db76",
"platform": "kotlin",
"rationale": "Android-only capture constant for the never-received 0x8b spo2_r_pi tag (OURA_PROTOCOL.md s6.5.1): the history drain logs its first raw payloads as decoder fixtures. No layout is decoded or stored."
},
{
"type": "platform_specific",
"kind": "add-unpaired-function",
Expand Down Expand Up @@ -424,6 +416,126 @@
"identity_sha256": "66bff0ba4ddae17d3628a64dcd7e16e27d5f6b979b0971f36a92d5111b379bb9",
"platform": "kotlin",
"rationale": "Kotlin twin of the Swift OuraActivityEstimator (Packages/OuraProtocol/.../ActivityEstimate.swift). Byte-identical output is pinned by OuraActivityEstimatorTest against the verbatim stdout of the Swift source compiled standalone over 300 generated inputs; the ledger cannot pair it by name because both platforms overload `estimate`."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/OuraSpO2Ratio.kt::COEF_A",
"identity_sha256": "a2056e203b381c266ca9fced680aeb0a7a7c7543a42cc22b46c9eda98e709fcc",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/OuraSpO2Ratio.kt::COEF_B",
"identity_sha256": "51718cd3f6b5f9f87080fc1eba1e0e183450a7fc8337f27e8818b66e4ecc6ed3",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/OuraSpO2Ratio.kt::COEF_C",
"identity_sha256": "fa9e607f89e17d69777e680f8f7efd8f8985420c192d6c5652d2640ccccce277",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/OuraSpO2Ratio.kt::MAX_PERCENT",
"identity_sha256": "73e5812e7a5f4625fb87a0e766fc6b093b8ae6d6ef623206c85c76dc4e9d2d9e",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/OuraSpO2Ratio.kt::MIN_PERCENT",
"identity_sha256": "816ea049dde93987ac87346ede773b28a9daa58c8da521a137e9e75c830ad2cc",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-file",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/OuraSpO2Ratio.kt",
"identity_sha256": "b0d142779a18682c88f529ad0d49551e5b9d2c1a44bcf0b1fba721e5164bb46c",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-function",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/Decoders.kt::decodeSpO2RatioPi/1#1",
"identity_sha256": "52d3b869b4ac64313faef0c0a61e4ea86f2dbf7a725d9540ed83dabad435fe59",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-function",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/oura/OuraSpO2Ratio.kt::percent/1#1",
"identity_sha256": "c513f2be9fff544a5dbee95f54f28458bec979b5a3c57292191ac3f88f1970a7",
"platform": "kotlin",
"rationale": "Android-only decode of the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1) into a phone-derived SpO2 percentage (gen4 quadratic cited from [open_oura-spo2]), tagged r_pi_pct and surfaced only through the SpO2 estimate display path. The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/analytics/OuraSpO2Nightly.kt::PI_SATURATED",
"identity_sha256": "feee8e608022c383aeefe7a840e7497f3640db2617bbf6ce88d4b69abbd0ccce",
"platform": "kotlin",
"rationale": "Android-only storage and nightly estimate for the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1): records are kept verbatim as OURA_SPO2_RPI events and converted at read time into the Oura SpO2 estimate (never spo2Pct, never scored). The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-constant",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/data/OuraStreamMapping.kt::EVENT_SPO2_RPI",
"identity_sha256": "26f18c6f9913d8c83e972c3634a1606a0afcbeb5d597b4a648261fbe03d45c80",
"platform": "kotlin",
"rationale": "Android-only storage and nightly estimate for the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1): records are kept verbatim as OURA_SPO2_RPI events and converted at read time into the Oura SpO2 estimate (never spo2Pct, never scored). The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-file",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/analytics/OuraSpO2Nightly.kt",
"identity_sha256": "a16ec04e9d1b7686c3124bc55776e0e118509f00793dbc90d02c3e08b26f088f",
"platform": "kotlin",
"rationale": "Android-only storage and nightly estimate for the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1): records are kept verbatim as OURA_SPO2_RPI events and converted at read time into the Oura SpO2 estimate (never spo2Pct, never scored). The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-function",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/analytics/IntelligenceEngine.kt::spo2CandidateMean/7#1",
"identity_sha256": "e841530d08842be977ebce724b46f9f262179fbb267da49ed6a044ca7d491fcd",
"platform": "kotlin",
"rationale": "Android-only storage and nightly estimate for the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1): records are kept verbatim as OURA_SPO2_RPI events and converted at read time into the Oura SpO2 estimate (never spo2Pct, never scored). The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-function",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/analytics/OuraSpO2Nightly.kt::displayMean/1#1",
"identity_sha256": "0621d0de852cbe823ae18586bf47c6f051ea174f233bf5ca4f86184ef9b73b70",
"platform": "kotlin",
"rationale": "Android-only storage and nightly estimate for the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1): records are kept verbatim as OURA_SPO2_RPI events and converted at read time into the Oura SpO2 estimate (never spo2Pct, never scored). The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-function",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/analytics/OuraSpO2Nightly.kt::line/1#1",
"identity_sha256": "93eeafaac3d4b2801eb6d7b3550a676417f4d1a1796d24a4090f993397dca6f6",
"platform": "kotlin",
"rationale": "Android-only storage and nightly estimate for the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1): records are kept verbatim as OURA_SPO2_RPI events and converted at read time into the Oura SpO2 estimate (never spo2Pct, never scored). The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
},
{
"type": "platform_specific",
"kind": "add-unpaired-function",
"identity": "kotlin\u0000android/app/src/main/java/com/noop/analytics/OuraSpO2Nightly.kt::summary/2#1",
"identity_sha256": "a77f37236a04c46f1900b9279980bd468a5ec3a357e02e9d2960a677cc81e21c",
"platform": "kotlin",
"rationale": "Android-only storage and nightly estimate for the Ring 4 0x8b spo2_r_pi stream (OURA_PROTOCOL.md s6.5.1): records are kept verbatim as OURA_SPO2_RPI events and converted at read time into the Oura SpO2 estimate (never spo2Pct, never scored). The fork's SpO2 feature-mode enable is Android-only, so only this platform receives 0x8b."
}
]
}
12 changes: 6 additions & 6 deletions Tools/parity_twin_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
]
},
"authority": {
"files": {"count": 506, "sha256": "7c1aa896ae06001a3869be1164233da7d6af9f909334334a917be56b4832c511"},
"functions": {"count": 4503, "sha256": "3cd89d9b79d6228b0e44fc2c89b272eb76a54a903e805bcec142f6da416cbce5"},
"files": {"count": 508, "sha256": "b7ab504f627dff2fb8c43c5021fd3ab02c75106f76ed9c6d15cd36e90cfce80a"},
"functions": {"count": 4508, "sha256": "3ce3f292737744b7395a8c8e1225db4dd09e7d5c5ef18a216cce470dad5113c2"},
"properties": {"count": 464, "sha256": "85338b70168a36af785255eb8959a41272cc02334529c2e3ef2a7d18444c7ef7"},
"constants": {"count": 1968, "sha256": "2c88ab0d322028ad38a0bdf4118894231ecbb7200ab28afe095e5de336a8d7bb"},
"constants": {"count": 1974, "sha256": "6126e173cc02eb2385ceb7332b349f1522f9dcaad7ba786b0c58ffca3240f9dc"},
"file_pairs": {"count": 74, "sha256": "1023813dc72dbe4122281d5bae27ac2b7a657dae5904cb5fd603032928199f94"},
"function_pairs": {"count": 189, "sha256": "11df8bf87b16b535aeddd0c00471f87431236fd0b059484c07f6d5939ace9732"},
"property_pairs": {"count": 149, "sha256": "8a62616eca6270e3d8899c47cfe8ae93691f16846ed1959e19a762827b6ec185"},
"constant_pairs": {"count": 681, "sha256": "3484c8096ac8d2832e75f18900c052a19bfb7d92cbc2fa7fdb6137d8fa46ebf6"},
"unpaired_files": {"count": 380, "sha256": "6e9cd41c3ee7c16a244e5ef95ff80cb31b27838afbab860416754bc91559adfe"},
"unpaired_functions": {"count": 4137, "sha256": "e4abeab38983324cb9716af8d6be4518ca80ce123f13785ee27d2795be306a00"},
"unpaired_files": {"count": 382, "sha256": "81e8923060fd20e5a887308d37750f5112f3804f15560833d4c2856f89e2871f"},
"unpaired_functions": {"count": 4142, "sha256": "b583ba53e9df860dd4a91e502cbb557bb506ae398c5a8a704770cded8968f206"},
"unpaired_properties": {"count": 166, "sha256": "280d96f0905fd623dfb66af86c99c3e82409b126a45aaf9e008eb8f6cf04671a"},
"unpaired_constants": {"count": 606, "sha256": "d334a29da52e658603db294deb3ff23e9625d682b1914703fc3bb09ebc97ac9a"}
"unpaired_constants": {"count": 612, "sha256": "c6c27a5ea84b9b0ac0e13379f68fa5625c95dc5e45652e7658dcdd439d53c777"}
}
}
2 changes: 1 addition & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ android {
applicationId = "com.noop.whoop"
minSdk = 26
targetSdk = 34
versionCode = 539
versionCode = 541
versionName = "11.8.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,18 @@ object IntelligenceEngine {
spo2: List<com.noop.data.Spo2Sample>,
from: Long,
to: Long,
diag: (String) -> Unit,
): Int? {
if (DeviceBrandCatalog.isOura(owner)) {
// Ring 4: the verbatim 0x8b R-ratio records (OURA_SPO2_RPI) when the night has any; otherwise the
// firmware 0x6F percentages through the ceiling@100 transform (Gen 3).
val ratioRows = runCatching {
repo.eventsByKind(owner, com.noop.data.OuraStreamMapping.EVENT_SPO2_RPI, from, to, STREAM_LIMIT)
}.getOrDefault(emptyList())
OuraSpO2Nightly.summary(sessions, ratioRows)?.let {
diag(OuraSpO2Nightly.line(it))
return OuraSpO2Nightly.displayMean(it)
}
return AnalyticsEngine.nightlySpo2CeilingMean(sessions, spo2)?.first
}
val auxSamples = repo.v18AuxSamples(owner, from, to, STREAM_LIMIT)
Expand Down Expand Up @@ -1399,7 +1409,7 @@ object IntelligenceEngine {
// bytes of the JVM's 64 KB per-method ceiling, and this block plus the #1575 trace recorders
// put the JaCoCo-instrumented size 30 bytes OVER the budget #1524 guards. Neither change
// exceeded it alone — only together, which no single PR's CI could see.
spo2CandidateMean(repo, owner, res.sleepSessions, spo2, from, to)?.let {
spo2CandidateMean(repo, owner, res.sleepSessions, spo2, from, to, ::dayDiag)?.let {
spo2CandidateByDay[res.daily.day] = it
}
}
Expand Down
55 changes: 55 additions & 0 deletions android/app/src/main/java/com/noop/analytics/OuraSpO2Nightly.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.noop.analytics

import com.noop.data.EventRow
import com.noop.data.OuraStreamMapping
import com.noop.oura.OuraSpO2Ratio
import kotlin.math.roundToInt
import org.json.JSONObject

/**
* Nightly SpO2 ESTIMATE from the Ring 4's verbatim `0x8b` records (OURA_SPO2_RPI events, OURA_PROTOCOL.md
* s6.5.1). A record counts when its anchored time falls inside a detected sleep session; every sample in it is
* converted with [OuraSpO2Ratio.percent]. A nightly low / median / mean / high needs no per-second timing, so
* the unpinned within-record spacing does not matter here. Diagnostic estimate only: never `spo2Pct`, never
* scored. Android-only.
*/
internal object OuraSpO2Nightly {
/** Samples used, and the night's low / median / mean / high in whole percent (median and mean unrounded). */
data class Summary(val samples: Int, val low: Int, val median: Double, val mean: Double, val high: Int)

/**
* Perfusion-index byte marking a held / no-signal sample: in the first Ring 4 capture all 6 of 64 such
* samples carried the same R (0.766, ~93 %) while every other sample varied. Inferred, not documented.
*/
const val PI_SATURATED = 0xFF

fun summary(sessions: List<DetectedSleep>, rows: List<EventRow>): Summary? {
if (sessions.isEmpty() || rows.isEmpty()) return null
val values = ArrayList<Int>()
for (row in rows) {
if (row.kind != OuraStreamMapping.EVENT_SPO2_RPI) continue
if (sessions.none { row.ts in it.start..it.end }) continue
val o = runCatching { JSONObject(row.payloadJSON) }.getOrNull() ?: continue
val r = o.optJSONArray("r_x16384") ?: continue
val pi = o.optJSONArray("pi") ?: continue
if (r.length() != pi.length()) continue
for (i in 0 until r.length()) {
if (pi.getInt(i) == PI_SATURATED) continue
values.add(OuraSpO2Ratio.percent(r.getInt(i) / 16384.0))
}
}
if (values.isEmpty()) return null
values.sort()
val n = values.size
val median = if (n % 2 == 1) values[n / 2].toDouble() else (values[n / 2 - 1] + values[n / 2]) / 2.0
return Summary(n, values.first(), median, values.average(), values.last())
}

/** The strap-log line for one night's summary (counts and percentages only). */
fun line(s: Summary): String =
"spo2 r-pi night samples=${s.samples} low=${s.low} median=${"%.1f".format(java.util.Locale.ROOT, s.median)} " +
"mean=${"%.1f".format(java.util.Locale.ROOT, s.mean)} high=${s.high} (0x8b estimate, not scored)"

/** The whole-percent value the SpO2 estimate tile shows. */
fun displayMean(s: Summary): Int = s.mean.roundToInt()
}
Loading
Loading