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
6 changes: 6 additions & 0 deletions Strand/BLE/BLEManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5927,6 +5927,9 @@ extension BLEManager: @preconcurrency CBCentralManagerDelegate {
// re-derives it. (The honest flag is per-link, like the syncing pill / reject counters above.)
whoop5EmptyOffload.reset()
state.historySyncExperimental = false
// #689/#815: the backlog sample is "at connect" by definition, so it must not survive the link it
// was taken on — a stale figure under a fresh connection would be a plain lie.
state.pagesBehindAtConnect = nil
lastBatteryReadAt = nil // #battery: next connect's first enableLiveNotifications re-seeds the 5/MG battery reading
// #612: the display flag only, not the underlying emptySyncTracker streak (that counter
// deliberately survives a reconnect — unchanged, existing behaviour). A fresh link re-derives
Expand Down Expand Up @@ -6719,6 +6722,9 @@ extension BLEManager: @preconcurrency CBPeripheralDelegate {
if !DataRange.isPendingResponse(frame, cmdOff: cmdOff) {
if let pages = DataRange.pagesBehind(from: frame, cmdOff: cmdOff) {
log("Strap backlog pages behind: \(pages) (#689 — GET_DATA_RANGE ring backlog, diagnostic only)")
// #815: confirmed on both WHOOP 4.0 and 5.0/MG, so bank it unconditionally. The Today sync
// chip reads this while backfilling is true. Twin of the Android FrameRouter branch.
state.pagesBehindAtConnect = Int(pages)
} else {
log("Strap backlog pages behind: not decodable from this frame (#689 — offsets may have moved; "
+ "the raw frame above is the input). Diagnostic only, sync is unaffected.")
Expand Down
10 changes: 10 additions & 0 deletions Strand/BLE/LiveState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,16 @@ public final class LiveState: ObservableObject {
/// experimental on 5.0" instead of a WHOOP-4-style "not recording"/sync-error. Reset on connect/disconnect.
@Published public var historySyncExperimental: Bool = false

/// #689/#815 — the strap's ring-buffer page backlog, sampled ONCE from the connect-time
/// GET_DATA_RANGE reply and never re-polled mid-offload: the link is already firmware-paced, and #377
/// rules out re-polling just to feed a readout. So this is a figure AT CONNECT rather than a live one,
/// and the Today sync chip's copy says so — a static number under a "syncing" label otherwise reads as
/// a stalled live one. A bounded ring measure (write pointer − read pointer against the ring size),
/// never a percentage: the strap never reveals a total record count. Confirmed against real captures
/// on WHOOP 4.0 and 5.0/MG. nil before the first reply this session, or when the frame did not decode.
/// Twin of Android `LiveState.pagesBehindAtConnect`.
@Published public var pagesBehindAtConnect: Int? = nil

/// #612 — true when the WHOOP-4/generic empty-offload streak (`EmptySyncTracker`, `BLEManager`) is
/// currently SUSTAINED (3+ consecutive completed-but-empty offloads). Not 5/MG-specific and not
/// coupled to HR: a connected strap that keeps handing over nothing has this true regardless of
Expand Down
16 changes: 13 additions & 3 deletions Strand/Liquid/LiquidTodayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2735,9 +2735,19 @@ private struct LiquidBatteryButton: View {
// VoiceOver announcing a real count while the ring showed the synthetic one — i.e. the
// harness could not be used to check the read-out it exists to exercise.
let n = syncChunks
return n > 0
? String(localized: "Syncing strap history, \(n) chunks")
: String(localized: "Syncing strap history")
guard n > 0 else { return String(localized: "Syncing strap history") }
// #689/#815: the connect-time ring backlog, when the strap reported one. Zero is dropped by
// `SyncChipState.resolve`, and dropped here for the same reason: "0 pages behind" beside a
// running sync contradicts itself. Both counts inflect — the phrase is built from its own
// entry and joined through a template, so "1 chunk" and "1 page" read correctly and the
// joining punctuation stays inside the translated template rather than being concatenated.
let behind = live.pagesBehindAtConnect
.flatMap { $0 > 0 ? $0 : nil }
.map { String(localized: "\($0) pages behind at connect") }
if let behind {
return String(localized: "Syncing strap history, \(n) chunks, \(behind)")
}
return String(localized: "Syncing strap history, \(n) chunks")
}

switch batteryDisplay {
Expand Down
Loading