Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -210,32 +210,26 @@ public enum ConnectionReadout {
return nil
}

/// #987/#261: the "clock latched" readout value. "yes" once EITHER signal lands with a plausible
/// (post-1972) timestamp: a GET_CLOCK correlation (`deviceClockUnix`, the WHOOP4 path), or a
/// GET_DATA_RANGE reply's newest banked record (`strapNewestUnix`, the fallback). "no (RTC reads
/// 1970/71)" when whichever signal landed reads epoch-era; "no (waiting for the strap clock)" before
/// either replies.
///
/// A WHOOP 5/MG's GET_CLOCK reply rides the puffin notify channel and never reaches the WHOOP4-only
/// correlation path that sets `deviceClockUnix` (see `BLEManager`'s connect-handshake comment) — its
/// records carry absolute timestamps, so it never NEEDS that correlation to decode history. Without
/// this fallback the row read "no (waiting for the strap clock)" forever on every 5/MG, even a fully
/// working one, because the one signal it checked structurally never populates for that family. The
/// data-range reply is an equal-weight proof the strap answered with a working clock, not a downgrade
/// — `rtcWarning` below already trusts it the same way.
public static func clockLatchedLabel(deviceClockUnix: Int?, strapNewestUnix: Int? = nil) -> String {
if let d = deviceClockUnix {
return d < ConnectionTrace.rtcEpochCeilingUnix ? "no (RTC reads 1970/71)" : "yes"
/// The current link's clock evidence, including future-dated and stale records. A post-1972
/// timestamp alone is not proof that the clock is right (#2091).
/// Kotlin twin: `ConnectionReadout.clockStatusLabel`.
public static func clockStatusLabel(deviceClockUnix: Int?, strapNewestUnix: Int?, nowUnix: Int) -> String {
guard deviceClockUnix != nil || strapNewestUnix != nil else { return "waiting for clock evidence" }
let clock = deviceClockUnix.map { unix in
ConnectionTrace.clockVerdict(aheadSeconds: unix - nowUnix, newestUnix: unix,
futureToleranceSeconds: 120,
behindToleranceSeconds: ConnectionTrace.behindToleranceDefault)
}
// #1823: this branch has NOT read a clock. It is reached when no clock correlation exists - which
// is every 5/MG, whose GET_CLOCK reply rides the puffin notify chars and never touches the WHOOP4
// correlation path - so the only evidence is how the strap DATED its banked records. Saying "RTC
// reads 1970/71" there claimed a reading we never took, in the one readout a reporter quotes when
// asking why nothing syncs. Report the evidence we actually have.
if let n = strapNewestUnix {
return n < ConnectionTrace.rtcEpochCeilingUnix ? "no (records dated 1970/71)" : "yes"
let record = strapNewestUnix.map { unix in
ConnectionTrace.clockVerdict(aheadSeconds: unix - nowUnix, newestUnix: unix,
futureToleranceSeconds: 120,
behindToleranceSeconds: ConnectionTrace.behindToleranceDefault)
}
return "no (waiting for the strap clock)"
if clock?.contains("FUTURE-DATED") == true || record?.contains("FUTURE-DATED") == true { return "future-dated" }
if clock?.contains("RTC-EPOCH") == true { return "RTC reads 1970/71" }
if record?.contains("RTC-EPOCH") == true { return "records dated 1970/71" }
if record?.contains("CLOCK-WARNING") == true { return "record is over 48h old" }
return strapNewestUnix != nil ? "records dated normally" : "clock OK"
}

/// #1818: at or above this charge the "charge it" remedy is already satisfied, so repeating it is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,33 +191,6 @@ final class ConnectionReadoutTests: XCTestCase {
XCTAssertNil(ConnectionReadout.clockCorrelatedDevice(logLines: ["connect up"]))
}

func testClockLatchedLabel() {
XCTAssertEqual(ConnectionReadout.clockLatchedLabel(deviceClockUnix: 1_782_475_600), "yes")
XCTAssertEqual(ConnectionReadout.clockLatchedLabel(deviceClockUnix: 40_000_000), "no (RTC reads 1970/71)")
XCTAssertEqual(ConnectionReadout.clockLatchedLabel(deviceClockUnix: nil), "no (waiting for the strap clock)")
}

// #261: a WHOOP 5/MG never populates deviceClockUnix (its GET_CLOCK reply rides the puffin channel,
// never the WHOOP4 correlation path) — the data-range fallback is what keeps the row from reading
// "waiting" forever on a strap that's actually fine.
func testClockLatchedLabelFallsBackToStrapNewestForFiveMG() {
XCTAssertEqual(
ConnectionReadout.clockLatchedLabel(deviceClockUnix: nil, strapNewestUnix: 1_782_475_600),
"yes")
// #1823: no clock was READ on this path - it is the 5/MG fallback, where the only evidence is
// how the strap dated its records. The wording must not claim a clock reading.
XCTAssertEqual(
ConnectionReadout.clockLatchedLabel(deviceClockUnix: nil, strapNewestUnix: 40_000_000),
"no (records dated 1970/71)")
XCTAssertEqual(
ConnectionReadout.clockLatchedLabel(deviceClockUnix: nil, strapNewestUnix: nil),
"no (waiting for the strap clock)")
// deviceClockUnix wins when BOTH signals are present (the WHOOP4 correlation is the more direct one).
XCTAssertEqual(
ConnectionReadout.clockLatchedLabel(deviceClockUnix: 1_782_475_600, strapNewestUnix: 40_000_000),
"yes")
}

func testRtcWarningFiresOnEpochEraClockOrNewest() {
XCTAssertNotNil(ConnectionReadout.rtcWarning(deviceClockUnix: 40_000_000, strapNewestUnix: nil))
XCTAssertNotNil(ConnectionReadout.rtcWarning(deviceClockUnix: nil, strapNewestUnix: 30_000_000))
Expand All @@ -226,6 +199,24 @@ final class ConnectionReadoutTests: XCTestCase {
"no signal seen yet must not fabricate a fault")
}

func testClockStatusRejectsFutureDatedRecords() {
XCTAssertEqual(ConnectionReadout.clockStatusLabel(deviceClockUnix: nil,
strapNewestUnix: nil,
nowUnix: 1_800_000_000), "waiting for clock evidence")
XCTAssertEqual(ConnectionReadout.clockStatusLabel(deviceClockUnix: 1_799_999_990,
strapNewestUnix: nil,
nowUnix: 1_800_000_000), "clock OK")
XCTAssertEqual(ConnectionReadout.clockStatusLabel(deviceClockUnix: nil,
strapNewestUnix: 1_800_003_600,
nowUnix: 1_800_000_000), "future-dated")
XCTAssertEqual(ConnectionReadout.clockStatusLabel(deviceClockUnix: nil,
strapNewestUnix: 40_000_000,
nowUnix: 1_800_000_000), "records dated 1970/71")
XCTAssertEqual(ConnectionReadout.clockStatusLabel(deviceClockUnix: nil,
strapNewestUnix: 1_799_999_990,
nowUnix: 1_800_000_000), "records dated normally")
}

/// #1818: the remedy must track the battery. A charged strap told to "charge to 100%" is the bug
/// the field report hit - the user had already done it, twice.
func testRtcWarningRemedyTracksBattery() {
Expand Down
9 changes: 5 additions & 4 deletions Strand/Screens/DevicesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,15 @@ private struct DevicesContent: View {
guard live.connected else { return nil }
let deviceClock = ConnectionReadout.clockCorrelatedDevice(logLines: live.log)
guard deviceClock != nil || live.strapRange != nil || live.lastFrameAtUnix != nil else { return nil }
let latched = ConnectionReadout.clockLatchedLabel(deviceClockUnix: deviceClock,
strapNewestUnix: live.strapRange?.newestUnix)
let latched = ConnectionReadout.clockStatusLabel(deviceClockUnix: deviceClock,
strapNewestUnix: live.strapRange?.newestUnix,
nowUnix: Int(Date().timeIntervalSince1970))
let frame = ConnectionReadout.lastFrameLabel(lastFrameUnix: live.lastFrameAtUnix,
nowUnix: Int(Date().timeIntervalSince1970))
let warning = ConnectionReadout.rtcWarning(deviceClockUnix: deviceClock,
strapNewestUnix: live.strapRange?.newestUnix,
batteryPct: live.batterySamples.last?.soc)
return (String(localized: "Clock latched: \(latched) · last frame \(frame)"), warning)
return (String(localized: "Clock") + ": \(latched) · last frame \(frame)", warning)
}

/// #802: same shape and copy as `LiveView.reconnectGuideBanner`. Duplicated rather than hoisted
Expand Down Expand Up @@ -760,7 +761,7 @@ private struct DeviceCard: View {
var liveFirmware: String? = nil
/// The active+connected strap's observed banked-history record layout (`hist_version`).
var liveHistoryLayout: Int? = nil
/// #987: the active+connected strap's clock-state line ("Clock latched: yes · last frame 12s ago"),
/// #987/#2091: the active+connected strap's clock-state line ("Clock: records dated normally · last frame 12s ago"),
/// nil for every other card. Built by the parent off the same pure ConnectionReadout parsers the
/// Test Centre Connection panel binds, so the two readouts can never disagree.
var liveClockLine: String? = nil
Expand Down
7 changes: 4 additions & 3 deletions Strand/Screens/TestCentreView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1088,9 +1088,10 @@ private struct ConnectionReadoutPanel: View {
ReadoutRow(label: String(localized: "Rows drained (session)"),
value: sessionRows.map(String.init) ?? String(localized: "no offload yet"))
ReadoutRow(label: String(localized: "Rows drained (all time)"), value: String(allTimeRows))
ReadoutRow(label: String(localized: "Clock latched"),
value: ConnectionReadout.clockLatchedLabel(deviceClockUnix: deviceClock,
strapNewestUnix: live.strapRange?.newestUnix))
ReadoutRow(label: String(localized: "Clock"),
value: ConnectionReadout.clockStatusLabel(deviceClockUnix: deviceClock,
strapNewestUnix: live.strapRange?.newestUnix,
nowUnix: now))
ReadoutRow(label: String(localized: "Last frame"),
value: ConnectionReadout.lastFrameLabel(lastFrameUnix: live.lastFrameAtUnix, nowUnix: now))
if let rtcWarning {
Expand Down
2 changes: 1 addition & 1 deletion Tools/parity_ledger_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{"rule": "test-only-callsite", "scope": "Packages/StrandImport", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/StrandImport; no wildcard matching.", "count": 14, "identities_sha256": "1acdc71178e0a555f000c42980345e8c38495f9e3806a866f31b260865d3ac98"},
{"rule": "test-only-callsite", "scope": "Packages/WhoopProtocol", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/WhoopProtocol; no wildcard matching.", "count": 29, "identities_sha256": "a13306c37eb1a9272b8666d09700d5120fb5fe0e629f3aaaeef649357f7f8b45"},
{"rule": "test-only-callsite", "scope": "Packages/WhoopStore", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/WhoopStore; no wildcard matching.", "count": 37, "identities_sha256": "2a62c1ece1c541b662dd407f0668d9a02782bb4d21f9bb181518272ff12f0e40"},
{"rule": "test-only-callsite", "scope": "android/analytics", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/analytics; no wildcard matching.", "count": 65, "identities_sha256": "fda58e5bb0fe07a9fa773d8e5f8961d483e8b177f1529b6e9dc3ac26df7c31cf"},
{"rule": "test-only-callsite", "scope": "android/analytics", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/analytics; no wildcard matching.", "count": 60, "identities_sha256": "56788639820a818ea805a5fa58ead50867b1c269bf5d3974ba3724704c38dde6"},
{"rule": "test-only-callsite", "scope": "android/data", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/data; no wildcard matching.", "count": 13, "identities_sha256": "6abaae19303cadccbe259d7db320850e269433472b28c84a1bfce7d32d54ce5f"},
{"rule": "test-only-callsite", "scope": "android/ingest", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/ingest; no wildcard matching.", "count": 12, "identities_sha256": "d4f45d66087768eab133b918af6658b06ec1d91305e38fb045f6ae1629c94678"},
{"rule": "test-only-callsite", "scope": "android/oura", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope android/oura; no wildcard matching.", "count": 4, "identities_sha256": "9beaa9034af0a1021b9a9f43739971934f260147e0a9a5c462468548a7fd577d"},
Expand Down
6 changes: 3 additions & 3 deletions Tools/parity_twin_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
},
"authority": {
"files": {"count": 509, "sha256": "0a0b02d39eb26ad0635933ad6fca51fb4c33db98178410bfe6676ab9d0fdcaee"},
"functions": {"count": 4571, "sha256": "ea0267f6ff16718fb08b7526558f27e525ed4fbebf6bbaf8ae199296094cc3a1"},
"functions": {"count": 4571, "sha256": "e1f7ab9c9bcd01d95b11f209fd4cf0ec0af6b83f1b57125947a0b2d00a397fd3"},
"properties": {"count": 496, "sha256": "18c74d4787010d269ae31f0ca2a9898b60cd5e79591bb82194007038c625054c"},
"constants": {"count": 2041, "sha256": "cd3d7c81c676d21d9bcc515f900ed585d68b8ba7e2440840b3fe52a702d4b8ef"},
"file_pairs": {"count": 81, "sha256": "3c94824ccff843da495a212a973d89ce61e536b6795dbee2d0e496a7e7a498f0"},
"function_pairs": {"count": 238, "sha256": "62681716d701dbd647daf6676dbbf32b6ae1e19f8a5b7ce177824619a61700bd"},
"function_pairs": {"count": 239, "sha256": "bec0088faad2f652b96291a1b6d2cedbd60a9966961d6cb484ca4d4972c22456"},
"property_pairs": {"count": 167, "sha256": "a6877b76b4ad9d4f6705421de0df1e17a06881c47b1d693a5cdb4161067ae8dd"},
"constant_pairs": {"count": 720, "sha256": "ac70a1ecc9b69f9d3b9a6f98ebbdf7ea3ab6a61c7ee03ba52b5d89f2ac3e6332"},
"unpaired_files": {"count": 369, "sha256": "3462a2a8135bfd793dc6493f10fb72b4fae9028ad9ec47c8dec84b9f5a4dc01a"},
"unpaired_functions": {"count": 4108, "sha256": "4c548bc990ef3288f4934076863bdb4d727caf885387f88f89aa5019c8a124df"},
"unpaired_functions": {"count": 4106, "sha256": "c1de50c27dbbf43d462583940b3fbaf61bf244fc8ddda7406aa33af280de7ec5"},
"unpaired_properties": {"count": 162, "sha256": "dabff97ba9bce0aebdc8ef84b9c4f107ce574020e8b0455d8f9214082948246b"},
"unpaired_constants": {"count": 601, "sha256": "13693d0d240b34610a5ab2c6b1dcea1d9702b9da5e9e3714b631ef3795e71a38"}
}
Expand Down
41 changes: 27 additions & 14 deletions android/app/src/main/java/com/noop/analytics/ConnectionReadout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,33 @@ object ConnectionReadout {
return null
}

/** #987/#261: the "clock latched" readout value: "yes" once EITHER signal lands with a plausible
* (post-1972) timestamp — a GET_CLOCK correlation (deviceClockUnix, the WHOOP4 path) or a
* GET_DATA_RANGE reply's newest banked record (strapNewestUnix, the fallback a WHOOP 5/MG needs
* since its GET_CLOCK reply never populates deviceClockUnix — see the Swift twin's doc comment for
* why). "no (RTC reads 1970/71)" on an epoch-era signal; "no (waiting for the strap clock)" before
* either replies. Twin of the Swift labeller. */
fun clockLatchedLabel(deviceClockUnix: Long?, strapNewestUnix: Long? = null): String {
val ceiling = ConnectionTrace.RTC_EPOCH_CEILING_UNIX
if (deviceClockUnix != null) return if (deviceClockUnix < ceiling) "no (RTC reads 1970/71)" else "yes"
// #1823: this branch has NOT read a clock - it is reached when no correlation exists, which is
// every 5/MG. The only evidence is how the strap DATED its records, so say that rather than
// claiming a clock reading we never took. Twin of the Swift wording.
if (strapNewestUnix != null) return if (strapNewestUnix < ceiling) "no (records dated 1970/71)" else "yes"
return "no (waiting for the strap clock)"
/** The current link's clock evidence, including future-dated and stale records. A post-1972
* timestamp alone is not proof that the clock is right (#2091).
* Swift twin: `ConnectionReadout.clockStatusLabel`. */
fun clockStatusLabel(deviceClockUnix: Long?, strapNewestUnix: Long?, nowUnix: Long): String {
if (deviceClockUnix == null && strapNewestUnix == null) return "waiting for clock evidence"
val clock = deviceClockUnix?.let { unix ->
ConnectionTrace.clockVerdict(
aheadSeconds = unix - nowUnix, newestUnix = unix,
futureToleranceSeconds = 120L,
behindToleranceSeconds = ConnectionTrace.BEHIND_TOLERANCE_DEFAULT,
)
}
val record = strapNewestUnix?.let { unix ->
ConnectionTrace.clockVerdict(
aheadSeconds = unix - nowUnix, newestUnix = unix,
futureToleranceSeconds = 120L,
behindToleranceSeconds = ConnectionTrace.BEHIND_TOLERANCE_DEFAULT,
)
}
return when {
clock?.contains("FUTURE-DATED") == true || record?.contains("FUTURE-DATED") == true -> "future-dated"
clock?.contains("RTC-EPOCH") == true -> "RTC reads 1970/71"
record?.contains("RTC-EPOCH") == true -> "records dated 1970/71"
record?.contains("CLOCK-WARNING") == true -> "record is over 48h old"
strapNewestUnix != null -> "records dated normally"
else -> "clock OK"
}
}

/** #1818: at or above this charge the "charge it" remedy is already satisfied, so repeating it is
Expand Down
Loading
Loading