Repository navigation
Add diagnostic WHOOP GATT family detection - #713
Merged
Merged
Conversation
mwong56
pushed a commit
to mwong56/noop
that referenced
this pull request
Jul 22, 2026
… platforms (ryanbr#250) * Predictive low-battery alert: warn at ~24h of estimated runtime left The fixed 15% low alert (ryanbr#368) gives ~16h of warning on a WHOOP 4.0 but ~1.8 days on a 5.0/MG. BatteryEstimator (ryanbr#713) already fits a personal runtime estimate from the banked SoC series; connect the two: - BatteryEstimator.runtimeAlert (Swift + Kotlin twins, same fixtures): fire when remainingHours <= 24 and not confirmed charging, re-arm only at >= 36 so slope jitter can't re-fire; suppressed-while-charging does not consume the once-per-discharge gate. - iOS/macOS: BatteryNotifier.onRuntimeEstimate posts "~22h left on your WHOOP" with the live figure; evaluated in AppModel's existing battery hook where the just-banked estimate is fresh. - Android: BatteryAlertNotifier.onRuntimeEstimate (id 4205) fed from WhoopConnectionService, re-fitting from the persisted battery table only when the SoC actually changes (~8-min strap cadence); same samples/rated inputs as the Today badge. - The 15% alert stays as the safety net (covers the nil-estimate case); everything remains behind the existing "Battery alerts" setting. Verified: StrandAnalytics swift test (980 passing, 7 new), Strand and NOOPiOS schemes compile. Android JVM tests are behavior-identical mirrors of the Swift fixtures; no Android toolchain on this machine, so Kotlin is signature-checked against the existing call-site patterns but not compiled here. * Granular sub-toggle for the predictive runtime alert (default ON) Review ask on ryanbr#250: the predictive ~24h "recharge tonight" warning can now be silenced on its own without losing the 15% low / 100% full alerts. - iOS/macOS: behavior.batteryPredictiveAlerts in BehaviorStore (default true), ANDed with behavior.batteryAlerts in the enabled value AppModel passes to BatteryNotifier.onRuntimeEstimate; "Predictive runtime warning" sub-row under Battery alerts in Automations (shown while the parent toggle is on). - Android twin: noop.batteryPredictiveAlerts in NoopPrefs (default true, suffix parity with the iOS key), checked at the top of BatteryAlertNotifier.onRuntimeEstimate alongside the existing batteryAlerts gate; matching sub-row in AutomationsScreen via AppViewModel. Kotlin written blind per the parity note (no Android toolchain here); Swift side verified with a clean NOOPiOS simulator build.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
… platforms (ryanbr#250) * Predictive low-battery alert: warn at ~24h of estimated runtime left The fixed 15% low alert (ryanbr#368) gives ~16h of warning on a WHOOP 4.0 but ~1.8 days on a 5.0/MG. BatteryEstimator (ryanbr#713) already fits a personal runtime estimate from the banked SoC series; connect the two: - BatteryEstimator.runtimeAlert (Swift + Kotlin twins, same fixtures): fire when remainingHours <= 24 and not confirmed charging, re-arm only at >= 36 so slope jitter can't re-fire; suppressed-while-charging does not consume the once-per-discharge gate. - iOS/macOS: BatteryNotifier.onRuntimeEstimate posts "~22h left on your WHOOP" with the live figure; evaluated in AppModel's existing battery hook where the just-banked estimate is fresh. - Android: BatteryAlertNotifier.onRuntimeEstimate (id 4205) fed from WhoopConnectionService, re-fitting from the persisted battery table only when the SoC actually changes (~8-min strap cadence); same samples/rated inputs as the Today badge. - The 15% alert stays as the safety net (covers the nil-estimate case); everything remains behind the existing "Battery alerts" setting. Verified: StrandAnalytics swift test (980 passing, 7 new), Strand and NOOPiOS schemes compile. Android JVM tests are behavior-identical mirrors of the Swift fixtures; no Android toolchain on this machine, so Kotlin is signature-checked against the existing call-site patterns but not compiled here. * Granular sub-toggle for the predictive runtime alert (default ON) Review ask on ryanbr#250: the predictive ~24h "recharge tonight" warning can now be silenced on its own without losing the 15% low / 100% full alerts. - iOS/macOS: behavior.batteryPredictiveAlerts in BehaviorStore (default true), ANDed with behavior.batteryAlerts in the enabled value AppModel passes to BatteryNotifier.onRuntimeEstimate; "Predictive runtime warning" sub-row under Battery alerts in Automations (shown while the parent toggle is on). - Android twin: noop.batteryPredictiveAlerts in NoopPrefs (default true, suffix parity with the iOS key), checked at the top of BatteryAlertNotifier.onRuntimeEstimate alongside the existing batteryAlerts gate; matching sub-row in AutomationsScreen via AppViewModel. Kotlin written blind per the parity note (no Android toolchain here); Swift side verified with a clean NOOPiOS simulator build.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…families Add diagnostic WHOOP GATT family detection
ryanbr
pushed a commit
that referenced
this pull request
Jul 27, 2026
…ght-guard (#864) Both existing battery alerts fire at most once per discharge cycle and then latch — the 15% SoC alert holds until the cell recovers to 25%, the 24 h predictive alert (#713) until the estimate returns to 36 h. Neither re-arms on anything but a charge, so a strap that just keeps draining goes silent exactly as the news gets worse. Measured on a 5.0 running R22 deep-data + high-rate IMU: both had already fired, then nothing across the final descent, and a full night was lost. At the measured 1.65 %/h that silence ran ~3 h with the app still showing ~6 h of runway. Two additions, each with its OWN persisted gate so a latched low/runtime alert can never suppress them. Critical SoC at 12%, derived from the hardware cutoff rather than picked round. The strap goes dark near 10% — the last HR sample on the reference run landed 27 min after the 10.9% reading and nothing below it was ever banked — so the usable band is (10%, 15%]. A conventional 5% is unreachable on this hardware and 10% is a coin flip. 12% sits mid-band, was reliably reported, and clears the 15% line by 3 pp so it is a genuinely separate crossing. Fired once on the real curve, 90 min before death. Bedtime night-guard near the LEARNED habitual bedtime (#547 model reused, not reinvented), firing when the strap will not clear wait-until-bed + the night + a margin. Re-arms on leaving the window, so once per NIGHT rather than once per charge — it speaks when everything else has latched. Cold start returns silent; no fabricated bedtime. Underneath both, usableRemainingHours re-anchors the estimate to the ~10% cutoff: remainingHours * (soc - cutoff) / soc, rearranged so the rate is never recovered. Verified numerically against computing it directly. The #99 clamp carries through conservatively — it only ever lowers remainingHours, so the usable figure can only be pessimistic. Added ADDITIVELY on purpose: remainingHours still feeds the Today badge and the Kotlin shared fixtures, so re-anchoring it in place would have moved a displayed number and broken documented parity in the same commit. Verified untouched. Parity checked mechanically, all six constants identical across platforms: cutoffSocPct 10.0, criticalSocPct 12, criticalRearmAbovePct 25, bedtimeLeadHours 3.0, bedtimeMarginHours 1.0, bedtimeMinNights 7. Android has no per-notification .timeSensitive; the shared channel is already IMPORTANCE_HIGH so both escalations heads-up, and setBypassDnd() is deliberately NOT taken since it needs user-granted policy access. Documented in the Kotlin rather than left as a silent divergence. The post() change is one defaulted parameter, so every existing call site is unchanged. Policy is pure and lives in StrandAnalytics, so swift test reaches it with no app, no strap, no CoreBluetooth. 10/10 CI green. KNOWN GAP, follow-up immediately: the four new Apple notification strings are not in Localizable.xcstrings, so they ship English on iOS while the Android twin is translated into all six locale files. i18n_audit cannot see this — it checks that entries already IN the catalog have every language, and a String(localized:) literal that never got extracted is invisible to it. That is #844 exactly. Two further literals in the same file were already missing before this PR.
ryanbr
added a commit
that referenced
this pull request
Jul 27, 2026
…e catalog (#866) Follow-up to #864. Resource-only, no code change. #864 added four String(localized:) literals in Strand/System/BatteryNotifier.swift and translated its Android twin into all five shipped locale files, but the Apple catalog was never updated — so both escalations shipped English on iOS/macOS in every language. Two further literals in the same file (the #713 runtime alert) were already missing before that PR. Six entries added, all eight shipped Apple languages: Strap battery low %@ left on your WHOOP — recharge tonight. Charge your WHOOP now %lld%% left. The strap stops recording near 10%% — it won't capture tonight unless you charge it. Won't last the night %@ of recording left, but tonight needs about %@. Charge before bed. WHY CI WAS GREEN ON #864. The audit checks that keys already IN a catalog carry every language. A String(localized:) literal that was never extracted is not a missing translation, it is a missing key, and is invisible to it. That is #844 exactly; this is the manual patch, not the fix. KEYS. Derived mechanically from the source literals rather than retyped: String interpolation to %@, Int to %lld, literal % to %% inside an interpolated string (a non-interpolated one keeps a bare %, which is why the existing "Your WHOOP is at 100%." is stored unescaped). That mapping is not assumed — it is the observed encoding of XiaomiBandView.swift:268, whose literal produced the existing key "%@ in bed · %lld%% efficiency", exercising all three conversions in one string. Keys stay non-positional and translations use %1$/%2$ where word order moves, matching how the catalog already stores its two-argument entries. REGISTER. de, fr and zh-Hans reuse the Android wording. es and pt-PT do not: #864 translated those formally, while this catalog is informal by a wide margin (es tu/tus/tú 762 against su/sus/usted 66; pt-PT teu/tua 689 against seu/sua/você 20) and the battery strings it already ships next to these are informal. Each language also uses the word for "strap" the catalog already established for it (Strap, pulsera, bracelet, fascia, correia, браслет, 手环, 手環) rather than importing Android's. Register and vocabulary only; no numbers move and no Android file is touched. GRAMMATICAL AGREEMENT. The substituted %@ is BatteryEstimator.label output — "~14h" or "~1h" — so a body opening with a verb must commit to singular or plural against a value it cannot see, and because the argument is an opaque string, xcstrings plural variations cannot key on it either. The three languages that inflect here are recast as label-then-value ("Autonomía restante en tu WHOOP: %@"). de, fr, ru and both Chinese variants were already agreement-free. Same treatment in the critical body, where %lld can be anything from 1 to criticalSocPct = 12, so both "Queda 1%" and "Quedan 12%" occur: it states the level instead. Two further wording fixes from a final read-through: the fr/it/pt-PT low-battery title placed the adjective after the strap noun, where it attaches to the strap and — since faible and scarica are also verb forms — can be read as a sentence about the strap discharging; and the es bedtime body ended in a feminine clitic with no antecedent, where every other Spanish string in this feature treats the device as masculine. Both now match how each language already renders the existing "Low battery" key. it, ru and zh-Hant are new translations and have NOT been checked by a native speaker. VERIFICATION. Resource-only, so nothing to compile, and both shipping Apple targets pick it up: Strand and NOOPiOS each take "- path: Strand" wholesale, excluding only Info.plist, entitlements and Assets.xcassets, so the catalog and BatteryNotifier.swift land in both. All 10 String(localized:) literals in that file now resolve to catalog keys. Argument types, positions and literal-percent counts were checked against every key across 6 keys x 8 languages after each rewrite, so no format string can garble at substitution. Measured against origin/main: 6 keys added, 0 removed, 0 pre-existing entries modified, and the catalog moves 3259 to 3265 keys with every per-language missing count unchanged. i18n_audit.py --ci origin/main green; Tools/i18n_extra_locale_baseline.txt untouched and unaffected. NOT IN SCOPE. Android hardcodes six battery strings as raw Kotlin literals rather than R.string (BatteryAlertNotifier.kt:123,124,151,152,163,164), so the runtime, low and full alerts ship English there in every locale — the same bug in the opposite direction. Filed as #867.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
… platforms (ryanbr#250) * Predictive low-battery alert: warn at ~24h of estimated runtime left The fixed 15% low alert (ryanbr#368) gives ~16h of warning on a WHOOP 4.0 but ~1.8 days on a 5.0/MG. BatteryEstimator (ryanbr#713) already fits a personal runtime estimate from the banked SoC series; connect the two: - BatteryEstimator.runtimeAlert (Swift + Kotlin twins, same fixtures): fire when remainingHours <= 24 and not confirmed charging, re-arm only at >= 36 so slope jitter can't re-fire; suppressed-while-charging does not consume the once-per-discharge gate. - iOS/macOS: BatteryNotifier.onRuntimeEstimate posts "~22h left on your WHOOP" with the live figure; evaluated in AppModel's existing battery hook where the just-banked estimate is fresh. - Android: BatteryAlertNotifier.onRuntimeEstimate (id 4205) fed from WhoopConnectionService, re-fitting from the persisted battery table only when the SoC actually changes (~8-min strap cadence); same samples/rated inputs as the Today badge. - The 15% alert stays as the safety net (covers the nil-estimate case); everything remains behind the existing "Battery alerts" setting. Verified: StrandAnalytics swift test (980 passing, 7 new), Strand and NOOPiOS schemes compile. Android JVM tests are behavior-identical mirrors of the Swift fixtures; no Android toolchain on this machine, so Kotlin is signature-checked against the existing call-site patterns but not compiled here. * Granular sub-toggle for the predictive runtime alert (default ON) Review ask on ryanbr#250: the predictive ~24h "recharge tonight" warning can now be silenced on its own without losing the 15% low / 100% full alerts. - iOS/macOS: behavior.batteryPredictiveAlerts in BehaviorStore (default true), ANDed with behavior.batteryAlerts in the enabled value AppModel passes to BatteryNotifier.onRuntimeEstimate; "Predictive runtime warning" sub-row under Battery alerts in Automations (shown while the parent toggle is on). - Android twin: noop.batteryPredictiveAlerts in NoopPrefs (default true, suffix parity with the iOS key), checked at the top of BatteryAlertNotifier.onRuntimeEstimate alongside the existing batteryAlerts gate; matching sub-row in AutomationsScreen via AppViewModel. Kotlin written blind per the parity note (no Android toolchain here); Swift side verified with a clean NOOPiOS simulator build.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…families Add diagnostic WHOOP GATT family detection
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ght-guard (ryanbr#864) Both existing battery alerts fire at most once per discharge cycle and then latch — the 15% SoC alert holds until the cell recovers to 25%, the 24 h predictive alert (ryanbr#713) until the estimate returns to 36 h. Neither re-arms on anything but a charge, so a strap that just keeps draining goes silent exactly as the news gets worse. Measured on a 5.0 running R22 deep-data + high-rate IMU: both had already fired, then nothing across the final descent, and a full night was lost. At the measured 1.65 %/h that silence ran ~3 h with the app still showing ~6 h of runway. Two additions, each with its OWN persisted gate so a latched low/runtime alert can never suppress them. Critical SoC at 12%, derived from the hardware cutoff rather than picked round. The strap goes dark near 10% — the last HR sample on the reference run landed 27 min after the 10.9% reading and nothing below it was ever banked — so the usable band is (10%, 15%]. A conventional 5% is unreachable on this hardware and 10% is a coin flip. 12% sits mid-band, was reliably reported, and clears the 15% line by 3 pp so it is a genuinely separate crossing. Fired once on the real curve, 90 min before death. Bedtime night-guard near the LEARNED habitual bedtime (ryanbr#547 model reused, not reinvented), firing when the strap will not clear wait-until-bed + the night + a margin. Re-arms on leaving the window, so once per NIGHT rather than once per charge — it speaks when everything else has latched. Cold start returns silent; no fabricated bedtime. Underneath both, usableRemainingHours re-anchors the estimate to the ~10% cutoff: remainingHours * (soc - cutoff) / soc, rearranged so the rate is never recovered. Verified numerically against computing it directly. The ryanbr#99 clamp carries through conservatively — it only ever lowers remainingHours, so the usable figure can only be pessimistic. Added ADDITIVELY on purpose: remainingHours still feeds the Today badge and the Kotlin shared fixtures, so re-anchoring it in place would have moved a displayed number and broken documented parity in the same commit. Verified untouched. Parity checked mechanically, all six constants identical across platforms: cutoffSocPct 10.0, criticalSocPct 12, criticalRearmAbovePct 25, bedtimeLeadHours 3.0, bedtimeMarginHours 1.0, bedtimeMinNights 7. Android has no per-notification .timeSensitive; the shared channel is already IMPORTANCE_HIGH so both escalations heads-up, and setBypassDnd() is deliberately NOT taken since it needs user-granted policy access. Documented in the Kotlin rather than left as a silent divergence. The post() change is one defaulted parameter, so every existing call site is unchanged. Policy is pure and lives in StrandAnalytics, so swift test reaches it with no app, no strap, no CoreBluetooth. 10/10 CI green. KNOWN GAP, follow-up immediately: the four new Apple notification strings are not in Localizable.xcstrings, so they ship English on iOS while the Android twin is translated into all six locale files. i18n_audit cannot see this — it checks that entries already IN the catalog have every language, and a String(localized:) literal that never got extracted is invisible to it. That is ryanbr#844 exactly. Two further literals in the same file were already missing before this PR.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…e catalog (ryanbr#866) Follow-up to ryanbr#864. Resource-only, no code change. ryanbr#864 added four String(localized:) literals in Strand/System/BatteryNotifier.swift and translated its Android twin into all five shipped locale files, but the Apple catalog was never updated — so both escalations shipped English on iOS/macOS in every language. Two further literals in the same file (the ryanbr#713 runtime alert) were already missing before that PR. Six entries added, all eight shipped Apple languages: Strap battery low %@ left on your WHOOP — recharge tonight. Charge your WHOOP now %lld%% left. The strap stops recording near 10%% — it won't capture tonight unless you charge it. Won't last the night %@ of recording left, but tonight needs about %@. Charge before bed. WHY CI WAS GREEN ON ryanbr#864. The audit checks that keys already IN a catalog carry every language. A String(localized:) literal that was never extracted is not a missing translation, it is a missing key, and is invisible to it. That is ryanbr#844 exactly; this is the manual patch, not the fix. KEYS. Derived mechanically from the source literals rather than retyped: String interpolation to %@, Int to %lld, literal % to %% inside an interpolated string (a non-interpolated one keeps a bare %, which is why the existing "Your WHOOP is at 100%." is stored unescaped). That mapping is not assumed — it is the observed encoding of XiaomiBandView.swift:268, whose literal produced the existing key "%@ in bed · %lld%% efficiency", exercising all three conversions in one string. Keys stay non-positional and translations use %1$/%2$ where word order moves, matching how the catalog already stores its two-argument entries. REGISTER. de, fr and zh-Hans reuse the Android wording. es and pt-PT do not: ryanbr#864 translated those formally, while this catalog is informal by a wide margin (es tu/tus/tú 762 against su/sus/usted 66; pt-PT teu/tua 689 against seu/sua/você 20) and the battery strings it already ships next to these are informal. Each language also uses the word for "strap" the catalog already established for it (Strap, pulsera, bracelet, fascia, correia, браслет, 手环, 手環) rather than importing Android's. Register and vocabulary only; no numbers move and no Android file is touched. GRAMMATICAL AGREEMENT. The substituted %@ is BatteryEstimator.label output — "~14h" or "~1h" — so a body opening with a verb must commit to singular or plural against a value it cannot see, and because the argument is an opaque string, xcstrings plural variations cannot key on it either. The three languages that inflect here are recast as label-then-value ("Autonomía restante en tu WHOOP: %@"). de, fr, ru and both Chinese variants were already agreement-free. Same treatment in the critical body, where %lld can be anything from 1 to criticalSocPct = 12, so both "Queda 1%" and "Quedan 12%" occur: it states the level instead. Two further wording fixes from a final read-through: the fr/it/pt-PT low-battery title placed the adjective after the strap noun, where it attaches to the strap and — since faible and scarica are also verb forms — can be read as a sentence about the strap discharging; and the es bedtime body ended in a feminine clitic with no antecedent, where every other Spanish string in this feature treats the device as masculine. Both now match how each language already renders the existing "Low battery" key. it, ru and zh-Hant are new translations and have NOT been checked by a native speaker. VERIFICATION. Resource-only, so nothing to compile, and both shipping Apple targets pick it up: Strand and NOOPiOS each take "- path: Strand" wholesale, excluding only Info.plist, entitlements and Assets.xcassets, so the catalog and BatteryNotifier.swift land in both. All 10 String(localized:) literals in that file now resolve to catalog keys. Argument types, positions and literal-percent counts were checked against every key across 6 keys x 8 languages after each rewrite, so no format string can garble at substitution. Measured against origin/main: 6 keys added, 0 removed, 0 pre-existing entries modified, and the catalog moves 3259 to 3265 keys with every per-language missing count unchanged. i18n_audit.py --ci origin/main green; Tools/i18n_extra_locale_baseline.txt untouched and unaffected. NOT IN SCOPE. Android hardcodes six battery strings as raw Kotlin literals rather than R.string (BatteryAlertNotifier.kt:123,124,151,152,163,164), so the runtime, low and full alerts ship English there in every locale — the same bug in the opposite direction. Filed as ryanbr#867.
ryanbr
added a commit
that referenced
this pull request
Aug 25, 2026
A WHOOP 4.0 field log banks two battery samples of 81% thirty seconds before the strap's own report says 39.2, then stays near 39 for the rest of the session. Those two are the only samples following a "Reading standard Battery Level (0x2A19)" line, which appears exactly twice and never again once the strap is identified. The 4.0's 0x2A19 is a stub rather than a state of charge (#77), but both readers asked the family question the wrong way round — `!= WHOOP4` — against a connectedFamily that defaults to WHOOP4, is written only at service discovery, and was never cleared per-connection, so between links it held the PREVIOUS strap's family. Asking that of a guess answers "use the standard characteristic" for a 4.0. It is the #171 shape the family resolver's own docs warn about: a family question answered by a fall-through rather than by evidence. Not cosmetic — banked samples feed the discharge-slope estimate (#713), so a phantom 81% at session start reaches the "time left" readout and lands in the history the open 4.0 drain work reads. Clear the establishment state per-connection alongside the other flags in reset(), and route both readers through one pure decision requiring positive evidence: defer while unknown, custom command for a 4.0, standard characteristic only for a link positively established as non-4.0. Deferring is safe: the battery is polled about every 60s, and the post-connect refresh sits inside an already-established 5/MG branch behind a 1.5s delay. reset() runs only in connectToDevice() and teardown, never between discovery and use. Also corrected the comment claiming that characteristic is a "constant 100". It returned 81 here — plausible enough to pass any range check, which is why the source and not the value has to be the gate. Android only. Swift has the identical gate shape but feeds it selectedModel, which has no generic case, defaults to .whoop4 (the safe branch), and is set by the service-filtered scan or adopt that reached the strap. The one residual — willRestoreState with a stale persisted model — is unevidenced and left for its own issue. Verified: app-build green both legs; Android build-and-test green; 4 new JVM tests, one sweeping every (established, family) pair to assert exactly one reaches the standard characteristic; doc_comment_lint and i18n_audit clean. Not strap-verified — the 81/39.2 pair above is the reproduction.
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
Refs ryanbr#713 # Conflicts: # Strand/App/TabRoute.swift # Strand/Liquid/LiquidTodayView.swift # Strand/Resources/Localizable.xcstrings # Strand/Screens/HomeDashboardContent.swift # Strand/Screens/SleepView.swift # StrandiOS/App/StrandiOSApp.swift # Tools/parity_twin_map.json # android/app/src/main/java/com/noop/ui/InsightsScreen.kt # android/app/src/main/java/com/noop/ui/SleepScreen.kt # android/app/src/main/java/com/noop/ui/SleepWhoopDetailUi.kt # android/app/src/main/res/values-de/strings.xml # android/app/src/main/res/values-es/strings.xml # android/app/src/main/res/values-fr/strings.xml # android/app/src/main/res/values-it/strings.xml # android/app/src/main/res/values-pl/strings.xml # android/app/src/main/res/values-pt-rPT/strings.xml # android/app/src/main/res/values-ru/strings.xml # android/app/src/main/res/values-zh/strings.xml # android/app/src/main/res/values/strings.xml
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
…g shell integration Refs ryanbr#713
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
Refs ryanbr#713 # Conflicts: # Tools/parity_dispositions.json # Tools/parity_ledger_baseline.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #688.
Adds diagnostic-only metadata and scan detection for the additional WHOOP GATT service families seen in the decompiled app:
puffin1150:11500001-6215-11ee-8c99-0242ac120002monument:8a580001-2fe8-4796-9267-b87a2b0c8234symphony:59830001-5955-419b-bb8d-c8262926af23The new families are deliberately not added as connectable
DeviceFamilycases.WHOOP4and fd4b Maverick/Goose remain the only connectable families.Behavior
puffin1150,monument, orsymphony.Naming
Keeps NOOP's existing fd4b "puffin" code names untouched. The separate official-app
PUFFINservice family is namedpuffin1150in docs/Swift andPUFFIN_1150in Kotlin to avoid colliding with fd4b packet framing terminology.Tests
swift test --package-path Packages/WhoopProtocol --filter DeviceFamilyFramingTestsJAVA_HOME=/opt/homebrew/Cellar/openjdk@21/21.0.11/libexec/openjdk.jdk/Contents/Home ANDROID_HOME=$HOME/Library/Android/sdk ./gradlew :app:testFullDebugUnitTest --tests 'com.noop.protocol.WhoopGattServiceFamilyTest'Additional note:
xcodebuild -project Strand.xcodeproj -scheme Strand -configuration Debug -destination 'platform=macOS' buildcurrently fails on an unrelated existingOuraActivityDumpmissing symbol inStrand/BLE/OuraLiveSource.swift, not in the files touched here.