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/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -208218,6 +208218,12 @@
} },
"A cardiovascular load from time in heart-rate zones (Edwards TRIMP): each zone is weighted so harder ones count for more, summed into one daily figure. Settings offers an exponential alternative (Banister) that credits short, hard efforts more.": { "localizations": {
"de": { "stringUnit": { "state": "translated", "value": "Eine kardiovaskuläre Last aus der Zeit in den Herzfrequenzzonen (Edwards-TRIMP): Jede Zone wird gewichtet, sodass härtere Zonen mehr zählen, aufsummiert zu einer Tageszahl. In den Einstellungen gibt es eine exponentielle Alternative (Banister), die kurze, harte Belastungen stärker anrechnet." } }, "en": { "stringUnit": { "state": "translated", "value": "A cardiovascular load from time in heart-rate zones (Edwards TRIMP): each zone is weighted so harder ones count for more, summed into one daily figure. Settings offers an exponential alternative (Banister) that credits short, hard efforts more." } }, "es": { "stringUnit": { "state": "translated", "value": "Una carga cardiovascular a partir del tiempo en las zonas de frecuencia cardíaca (TRIMP de Edwards): cada zona se pondera para que las más duras cuenten más, sumado en una sola cifra diaria. En Ajustes hay una alternativa exponencial (Banister) que valora más los esfuerzos cortos e intensos." } }, "fr": { "stringUnit": { "state": "translated", "value": "Une charge cardiovasculaire issue du temps passé dans les zones de fréquence cardiaque (TRIMP d’Edwards) : chaque zone est pondérée pour que les plus intenses comptent davantage, additionnée en un seul chiffre journalier. Les Réglages proposent une alternative exponentielle (Banister) qui valorise davantage les efforts courts et intenses." } }, "it": { "stringUnit": { "state": "translated", "value": "Un carico cardiovascolare dato dal tempo nelle zone di frequenza cardiaca (TRIMP di Edwards): ogni zona è ponderata in modo che quelle più dure contino di più, sommato in un unico dato giornaliero. Nelle Impostazioni c’è un’alternativa esponenziale (Banister) che premia di più gli sforzi brevi e intensi." } }, "pl": { "stringUnit": { "state": "translated", "value": "Obciążenie układu krążenia wynikające z czasu w strefach tętna (TRIMP Edwardsa): każda strefa jest ważona tak, aby trudniejsze liczyły się bardziej, zsumowane w jedną wartość dzienną. W Ustawieniach jest alternatywa wykładnicza (Banister), która mocniej docenia krótkie, intensywne wysiłki." } }, "pt-PT": { "stringUnit": { "state": "translated", "value": "Uma carga cardiovascular a partir do tempo nas zonas de frequência cardíaca (TRIMP de Edwards): cada zona é ponderada para que as mais difíceis contem mais, resumido num valor diário. Nas Definições existe uma alternativa exponencial (Banister) que valoriza mais os esforços curtos e intensos." } }, "ru": { "stringUnit": { "state": "translated", "value": "Сердечно-сосудистая нагрузка по времени в пульсовых зонах (TRIMP Эдвардса): каждая зона взвешивается так, что более тяжёлые учитываются сильнее, и всё суммируется в одно дневное значение. В настройках есть экспоненциальная альтернатива (Banister), которая сильнее засчитывает короткие интенсивные усилия." } }, "zh-Hans": { "stringUnit": { "state": "translated", "value": "根据你在各心率区间的时长得出的心血管负荷(Edwards TRIMP):区间越高权重越大,汇总为一个每日数字。设置中提供指数式替代方案(Banister),会更看重短时高强度的努力。" } }, "zh-Hant": { "stringUnit": { "state": "translated", "value": "根據你在各心率區間停留時間得出的心血管負荷(Edwards TRIMP):區間越高權重越大,加總成每日一個數字。設定中提供指數式替代方案(Banister),會更看重短時高強度的努力。" } }
} },
"Check what the strap has stored": { "localizations": {
"de": { "stringUnit": { "state": "translated", "value": "Prüfen, was das Band gespeichert hat" } }, "en": { "stringUnit": { "state": "translated", "value": "Check what the strap has stored" } }, "es": { "stringUnit": { "state": "translated", "value": "Comprobar qué tiene guardado la correa" } }, "fr": { "stringUnit": { "state": "translated", "value": "Vérifier ce que le bracelet a enregistré" } }, "it": { "stringUnit": { "state": "translated", "value": "Controlla cosa ha memorizzato il braccialetto" } }, "pl": { "stringUnit": { "state": "translated", "value": "Sprawdź, co zapisała opaska" } }, "pt-PT": { "stringUnit": { "state": "translated", "value": "Verificar o que a pulseira tem guardado" } }, "ru": { "stringUnit": { "state": "translated", "value": "Проверить, что сохранено на ремешке" } }, "zh-Hans": { "stringUnit": { "state": "translated", "value": "查看手环已存储的闹钟" } }, "zh-Hant": { "stringUnit": { "state": "translated", "value": "查看手環已儲存的鬧鐘" } }
} },
"The answer from the strap appears in your strap log and debug export, with the raw bytes it replied with.": { "localizations": {
"de": { "stringUnit": { "state": "translated", "value": "Die Antwort des Bands erscheint im Band-Log und im Diagnose-Export, samt der empfangenen Rohbytes." } }, "en": { "stringUnit": { "state": "translated", "value": "The answer from the strap appears in your strap log and debug export, with the raw bytes it replied with." } }, "es": { "stringUnit": { "state": "translated", "value": "La respuesta de la correa aparece en el registro y en la exportación de diagnóstico, con los bytes recibidos." } }, "fr": { "stringUnit": { "state": "translated", "value": "La réponse du bracelet apparaît dans le journal et l’export de diagnostic, avec les octets bruts reçus." } }, "it": { "stringUnit": { "state": "translated", "value": "La risposta del braccialetto compare nel log e nell’esportazione diagnostica, con i byte grezzi ricevuti." } }, "pl": { "stringUnit": { "state": "translated", "value": "Odpowiedź opaski pojawi się w logu i eksporcie diagnostycznym, wraz z otrzymanymi surowymi bajtami." } }, "pt-PT": { "stringUnit": { "state": "translated", "value": "A resposta da pulseira aparece no registo e na exportação de diagnóstico, com os bytes recebidos." } }, "ru": { "stringUnit": { "state": "translated", "value": "Ответ ремешка появится в журнале ремешка и диагностическом экспорте вместе с полученными байтами." } }, "zh-Hans": { "stringUnit": { "state": "translated", "value": "手环的回应会出现在手环日志和诊断导出中,并附带收到的原始字节。" } }, "zh-Hant": { "stringUnit": { "state": "translated", "value": "手環的回應會出現在手環日誌和診斷匯出中,並附上收到的原始位元組。" } }
} }
},
"version": "1.0"
Expand Down
21 changes: 21 additions & 0 deletions Strand/Screens/SmartAlarmView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,27 @@ struct SmartAlarmView: View {
.font(StrandFont.footnote)
.foregroundStyle(StrandPalette.textTertiary)
.frame(maxWidth: .infinity, alignment: .leading)
// #1706: ask the strap what it actually has stored. The readback was otherwise
// only reachable by ARMING, so anyone whose alarm is off could not produce the
// frame that explains a wrong reported time.
//
// Shown unconditionally, where the Android twin hides it until bonded. That is a
// deliberate platform difference, not drift: the Android card already observes
// LiveState, so gating there is free, while this view does not — and pulling
// `live` in would re-render the whole alarm screen on every 1 Hz HR tick, which
// this codebase keeps parent views clear of on purpose. `getStrapAlarm` no-ops
// and logs when nothing is connected, so the worst case is one ignored write.
Divider().overlay(StrandPalette.hairline)
Button {
model.ble.getStrapAlarm()
} label: {
Text("Check what the strap has stored")
}
.buttonStyle(NoopButtonStyle(.secondary, fullWidth: true))
Text("The answer from the strap appears in your strap log and debug export, with the raw bytes it replied with.")
.font(StrandFont.footnote)
.foregroundStyle(StrandPalette.textTertiary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions android/app/src/main/java/com/noop/ble/WhoopBleClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4526,6 +4526,27 @@ class WhoopBleClient(
}
}

/**
* Ask the strap what alarm it currently has stored (#1706). The reply lands on the command-notify
* characteristic and is handled by the same GET_ALARM_TIME branch an arm's own follow-up read uses,
* so it persists `alarm.lastReportedEpoch` + the RAW response bytes for the debug export.
*
* Port of Swift `BLEManager.getStrapAlarm`, which until now had no caller on either platform. It
* exists here because the readback was otherwise only reachable by ARMING: a user whose alarm is off
* could not produce the evidence needed to explain what their strap reports, and on Android arming
* was the sole trigger.
*
* Not family-gated, unlike [armStrapAlarm] — but that is defensive, not a feature. [send] already
* no-ops when nothing is connected, and only the 4.0 branch decodes the reply, so an errant call
* costs one ignored write. The single caller today is the 4.0 side of the strap-alarm card, so the
* 5/MG frame is NOT currently captured by anything; a caller could be added there if that layout
* ever needs recording, and this method would not have to change.
*/
fun getStrapAlarm() {
send(CommandNumber.GET_ALARM_TIME, byteArrayOf(0x01))
log("Alarm: requested current alarm time")
}

/**
* Arm the strap's **firmware** alarm to buzz at [epochSec] (absolute UTC seconds). The strap fires
* at that instant even if the phone is asleep or NOOP is closed. SET_CLOCK is sent first so the
Expand Down
16 changes: 16 additions & 0 deletions android/app/src/main/java/com/noop/ui/SmartAlarmScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,22 @@ private fun StrapAlarmCard(vm: AppViewModel) {
"Connect your strap to arm this; it's set on the strap's own firmware alarm. Confirmed working on WHOOP 4.0; still experimental on 5.0 and MG. Keep a backup alarm for anything you truly can't miss.",
style = NoopType.footnote, color = Palette.textTertiary,
)
// #1706: ask the strap what it actually has stored. The readback was previously only
// reachable by ARMING, so anyone whose alarm is off could not produce the evidence
// that explains a wrong reported time. 4.0 only — the 5/MG readback is not decoded.
if (live.bonded) {
RowDividerLocal()
NoopButton(
text = uiString(R.string.smart_alarm_check_strap_alarm),
kind = NoopButtonKind.Secondary,
fullWidth = true,
onClick = { vm.ble.getStrapAlarm() },
)
Text(
uiString(R.string.smart_alarm_check_strap_alarm_help),
style = NoopType.footnote, color = Palette.textTertiary,
)
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,8 @@
<string name="l10n_smart_alarm_screen_remind_me_to_wind_down_4839f0d0">Erinnere mich ans Runterkommen</string>
<string name="l10n_smart_alarm_screen_shorten_window_5bf5b37d">Fenster verkürzen</string>
<string name="l10n_smart_alarm_screen_strap_wake_alarm_1828fff3">Strap-Weckalarm</string>
<string name="smart_alarm_check_strap_alarm">Prüfen, was das Band gespeichert hat</string>
<string name="smart_alarm_check_strap_alarm_help">Die Antwort des Bands erscheint im Band-Log und im Diagnose-Export, samt der empfangenen Rohbytes.</string>
<string name="l10n_smart_alarm_screen_the_earliest_noop_will_wake_you_b641a2d4">Der früheste NOOP wird Sie wecken.</string>
<string name="l10n_smart_alarm_screen_the_guaranteed_alarm_fires_this_long_b0c51052">Der garantierte Alarm feuert so lange nach Ihrer frühesten Zeit.</string>
<string name="l10n_smart_alarm_screen_this_is_a_coarse_cue_from_d6bbabe7">Dies ist ein grober Hinweis auf die Herzfrequenz, keine klinische Schlafphase. Wenn das Gurtband</string>
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@
<string name="l10n_smart_alarm_screen_remind_me_to_wind_down_4839f0d0">Recuérdame relajarme</string>
<string name="l10n_smart_alarm_screen_shorten_window_5bf5b37d">Ventana corta</string>
<string name="l10n_smart_alarm_screen_strap_wake_alarm_1828fff3">Alarma de despertar de la pulsera</string>
<string name="smart_alarm_check_strap_alarm">Comprobar qué tiene guardado la pulsera</string>
<string name="smart_alarm_check_strap_alarm_help">La respuesta de la pulsera aparece en el registro y en la exportación de diagnóstico, con los bytes recibidos.</string>
<string name="l10n_smart_alarm_screen_the_earliest_noop_will_wake_you_b641a2d4">La primera NOOP te despertará.</string>
<string name="l10n_smart_alarm_screen_the_guaranteed_alarm_fires_this_long_b0c51052">La alarma garantizada dispara tanto tiempo después de su primer tiempo.</string>
<string name="l10n_smart_alarm_screen_this_is_a_coarse_cue_from_d6bbabe7">Este es un cue grueso de la frecuencia cardíaca, no una lectura clínica del estadio del sueño. Si la correa</string>
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@
<string name="l10n_smart_alarm_screen_remind_me_to_wind_down_4839f0d0">Me rappeler de me détendre</string>
<string name="l10n_smart_alarm_screen_shorten_window_5bf5b37d">Écourter la fenêtre</string>
<string name="l10n_smart_alarm_screen_strap_wake_alarm_1828fff3">Alarme de réveil du bracelet</string>
<string name="smart_alarm_check_strap_alarm">Vérifier ce que le bracelet a enregistré</string>
<string name="smart_alarm_check_strap_alarm_help">La réponse du bracelet apparaît dans le journal et l’export de diagnostic, avec les octets bruts reçus.</string>
<string name="l10n_smart_alarm_screen_the_earliest_noop_will_wake_you_b641a2d4">Le premier NOOP vous réveillera.</string>
<string name="l10n_smart_alarm_screen_the_guaranteed_alarm_fires_this_long_b0c51052">L\'alarme garantie tire aussi longtemps après votre plus jeune heure.</string>
<string name="l10n_smart_alarm_screen_this_is_a_coarse_cue_from_d6bbabe7">Il s\'agit d\'un signal grossier de la fréquence cardiaque, et non d\'une lecture clinique au stade du sommeil. Si le bracelet </string>
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values-pl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,8 @@
<string name="l10n_smart_alarm_screen_remind_me_to_wind_down_4839f0d0">Przypomnij mi, żebym się uspokoił</string>
<string name="l10n_smart_alarm_screen_shorten_window_5bf5b37d">Skróć okno</string>
<string name="l10n_smart_alarm_screen_strap_wake_alarm_1828fff3">Budzik na pasek</string>
<string name="smart_alarm_check_strap_alarm">Sprawdź, co zapisała opaska</string>
<string name="smart_alarm_check_strap_alarm_help">Odpowiedź opaski pojawi się w logu i eksporcie diagnostycznym, wraz z otrzymanymi surowymi bajtami.</string>
<string name="l10n_smart_alarm_screen_the_earliest_noop_will_wake_you_b641a2d4">Obudzi Cię najwcześniej NOOP.</string>
<string name="l10n_smart_alarm_screen_the_guaranteed_alarm_fires_this_long_b0c51052">Gwarantowany alarm uruchomi się tak długo po najwcześniejszym momencie.</string>
<string name="l10n_smart_alarm_screen_this_is_a_coarse_cue_from_d6bbabe7">Jest to przybliżona wskazówka na podstawie tętna, a nie kliniczny odczyt na etapie snu. Jeśli pasek</string>
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values-pt-rPT/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,8 @@
<string name="l10n_smart_alarm_screen_remind_me_to_wind_down_4839f0d0">Lembre-me de relaxar</string>
<string name="l10n_smart_alarm_screen_shorten_window_5bf5b37d">Encurtar janela</string>
<string name="l10n_smart_alarm_screen_strap_wake_alarm_1828fff3">Bracelete despertador</string>
<string name="smart_alarm_check_strap_alarm">Verificar o que a pulseira tem guardado</string>
<string name="smart_alarm_check_strap_alarm_help">A resposta da pulseira aparece no registo e na exportação de diagnóstico, com os bytes recebidos.</string>
<string name="l10n_smart_alarm_screen_the_earliest_noop_will_wake_you_b641a2d4">O primeiro NOOP irá acordá-lo.</string>
<string name="l10n_smart_alarm_screen_the_guaranteed_alarm_fires_this_long_b0c51052">O alarme garantido dispara muito depois do primeiro horário.</string>
<string name="l10n_smart_alarm_screen_this_is_a_coarse_cue_from_d6bbabe7">Esta é uma indicação grosseira da frequência cardíaca, não uma leitura clínica da fase do sono. Se a pega </string>
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,8 @@
<string name="l10n_smart_alarm_screen_remind_me_to_wind_down_4839f0d0">提醒我准备入睡</string>
<string name="l10n_smart_alarm_screen_shorten_window_5bf5b37d">缩短唤醒窗口</string>
<string name="l10n_smart_alarm_screen_strap_wake_alarm_1828fff3">手环无声唤醒</string>
<string name="smart_alarm_check_strap_alarm">查看手环已存储的闹钟</string>
<string name="smart_alarm_check_strap_alarm_help">手环的回应会出现在手环日志和诊断导出中,并附带收到的原始字节。</string>
<string name="l10n_smart_alarm_screen_the_earliest_noop_will_wake_you_b641a2d4">NOOP 可能会唤醒您的最早时间。</string>
<string name="l10n_smart_alarm_screen_the_guaranteed_alarm_fires_this_long_b0c51052">如果您在这段时间内未被智能唤醒,保底闹钟将在窗口期结束时必定响起。</string>
<string name="l10n_smart_alarm_screen_this_is_a_coarse_cue_from_d6bbabe7">这是根据心率变动做出的粗略提示,而非临床级别的睡眠阶段判断。如果手环...</string>
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,8 @@
<string name="l10n_smart_alarm_screen_remind_me_to_wind_down_4839f0d0">Remind me to wind down</string>
<string name="l10n_smart_alarm_screen_shorten_window_5bf5b37d">Shorten window</string>
<string name="l10n_smart_alarm_screen_strap_wake_alarm_1828fff3">Strap wake-alarm</string>
<string name="smart_alarm_check_strap_alarm">Check what the strap has stored</string>
<string name="smart_alarm_check_strap_alarm_help">The answer from the strap appears in your strap log and debug export, with the raw bytes it replied with.</string>
<string name="l10n_smart_alarm_screen_the_earliest_noop_will_wake_you_b641a2d4">The earliest NOOP will wake you.</string>
<string name="l10n_smart_alarm_screen_the_guaranteed_alarm_fires_this_long_b0c51052">The guaranteed alarm fires this long after your earliest time.</string>
<string name="l10n_smart_alarm_screen_this_is_a_coarse_cue_from_d6bbabe7">This is a coarse cue from heart rate, not a clinical sleep-stage reading. If the strap </string>
Expand Down
Loading