Repository navigation
Ios call buzz and coach fix - #2813
Open
ahmadssadiq wants to merge 7 commits into
Open
ahmadssadiq wants to merge 7 commits into
ahmadssadiq wants to merge 7 commits into
Conversation
The Coach streams its replies, and the streaming path in the OpenAI and Custom providers always sent `max_tokens` + `temperature`. Newer OpenAI models (the reasoning / GPT-5 families) reject that with: 400 - Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead. `send()` already retried that 400 with the modern parameter shape, but `stream()` had no retry (its comment said it fell back to `send`'s retry, which nothing did), so the Coach surfaced the raw provider error. `stream()` now mirrors `send()`: try the standard shape first, and on a 400 that mentions max_tokens / max_completion_tokens / temperature / "unsupported", re-stream with `max_completion_tokens` and no temperature. The 400 arrives before any delta is emitted, so re-streaming can't duplicate text. The body builder moved into a private `streamChat(...)` with a `modernParams` flag, matching the existing `chat(...)` helper. Builds and runs on an iPhone 16 Pro (iOS 26); not yet verified against a live OpenAI reasoning model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS counterpart of the Android incoming-call wrist alert (ryanbr#66): when a call rings on the iPhone, the WHOOP strap buzzes, like an Apple Watch, until the call is answered, declined or ends. Detection: CallKit's CXCallObserver. It sees every CallKit call on the phone (regular phone calls, FaceTime, and VoIP apps that ring through CallKit, e.g. WhatsApp voice and video) and needs no permission or entitlement. A call counts as ringing when it is incoming, not yet connected and not ended. Only that state is read: never the number, the caller or the app. Why not ANCS: ANCS has the accessory (the strap) subscribe to the iPhone's notification service, which needs WHOOP firmware support. The strap doesn't do that, so the phone-side app has to relay the call. Background behaviour: iOS only delivers CXCallObserver changes while the app runs. A backgrounded NOOP runs when the strap wakes it with a BLE frame, so BLEManager now fires `onInboundFrame` on every notification and the monitor re-checks the call list there (throttled to 1 Hz). With Settings > Continuous HRV capture on (and "Overnight only" off) the strap sends a frame every second, so alerts fire within about a second with the app in the foreground, in the background, or with the phone locked. A short beginBackgroundTask covers the start of each ring. It cannot work after the app is force-quit from the app switcher (iOS does not relaunch it). The monitor is created in AppModel.init, so it is live after a CoreBluetooth state-restoration relaunch too. Cadence: one burst (3 pulses, ~2 s on the strap per its own log) every 3 s while ringing, stopping within ~1 s of the call ending. A 90 s ceiling self-heals a call whose end is never observed. This differs from Android, which stops after 4 buzzes at 8 s intervals. Haptics (5/MG): the call buzz is the hardware-confirmed maverick notify buzz (cmd 0x13), written with response so a backgrounded link can't drop it, and without RUN_ALARM so repeats never start the alarm. It is one notch softer than other buzzes: the strap's driver is a TI DRV2625 (named in the strap log), and the call buzz uses ROM effect 48 ("Buzz 2 - 80%") instead of 47 ("Buzz 1 - 100%"). To support that: - MaverickHaptics.notificationBuzz(loops:effect:) takes the first waveform effect (default 47, so existing bodies are byte-identical; the second slot, 152 = 0x80|24, is a 240 ms pause and is unchanged). - BLEManager.send(_:payload:writeType:maverickEffect:) passes it through (default 47, so every existing caller is unchanged). Other strap families use the existing buzzStrapOnce() sequence. Settings: new "Buzz strap for incoming calls" toggle (default off, key notif.calls.enabled) under Auto-detect workouts, with captions covering what rings it, the privacy line, and the Continuous HRV capture requirement for background alerts. Tested on an iPhone 16 Pro (iOS 26) + WHOOP 5.0: the first version (single burst every few seconds) buzzed for phone calls with the app open, in the background and with the phone locked. The strap log showed the 0x13 frame and "HAPTICS: Sequence fired". The continuous cadence and the 80% effect are built and installed but not yet confirmed by feel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`xcodegen generate` adds $(PRODUCT_BUNDLE_IDENTIFIER).stalebattery to BGTaskSchedulerPermittedIdentifiers, because project.yml already declares it, but the committed Info.plist didn't have it. Committing the generated result so a fresh generate leaves the tree clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Personal preference for this fork, NOT intended for upstream.
With a WHOOP CSV export imported and Apple Health connected, every WHOOP
workout also exists as an identical Apple Health copy (the WHOOP app
writes its workouts into Health). Cross-source dedup only collapses
pairs with the same sport name, so pairs like WHOOP "Padel" vs Health
"Workout", or "Weightlifting" vs "Strength Training", showed twice.
- Repository.workoutNamespaces: drop apple-health, so Apple Health
workouts no longer appear in Workouts or on Today. Apple Health still
feeds steps, weight and calories. Side effect: auto-detect no longer
treats Apple Health workouts as already saved.
- WorkoutsView.currentScopeCount = Int.max: the "Current" tab shows
every session instead of the 10 most recent.
A proper upstream fix would be a setting, or collapsing time-identical
WHOOP/Apple pairs regardless of sport name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Personal build setting for this fork, NOT intended for upstream.
The NOOPiOS scheme embeds NOOPWatch, and xcodebuild refuses to build it
unless the watchOS SDK is installed ("watchOS 26.0 must be installed").
Commented out the NOOPWatch dependency of NOOPiOS so the iPhone app
builds on a Mac with only the iOS platform. Restore the line (and run
`xcodebuild -downloadPlatform watchOS`) to ship the watch app again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lets the Mac app show the iPhone's NOOP data without ever connecting to
the strap.
Why: a WHOOP holds an encrypted bond with one device at a time, and it
deletes history once a device has offloaded it. A Mac that also connects
takes the bond from the phone (no buzz, alarm or history sync there) and
splits the history between two stores that never merge. Mirror mode
keeps the phone as the only device on the strap, and gives the Mac the
phone's data through the existing Backup & Sync folder flow:
1. The iPhone backs up to a folder (iCloud Drive), daily or on
"Back up now" (unchanged).
2. On every Mac launch, before the store opens, the Mac restores the
newest .noopbak in that folder if it hasn't loaded it yet.
3. While mirroring, every Bluetooth scan/connect path refuses, and the
Mac never writes its own backups into the folder.
What changed:
- Strand/Data/MirrorMode.swift (new): the `mirror.enabled` toggle
(always false on iOS, which is the source), the launch step
`restoreNewestOnLaunch()`, and the status line shown in Backup &
Sync ("Loaded the iPhone backup from ...", "Up to date with ...",
"downloading from iCloud", "choose the folder", "no backups yet").
- FolderBackup.mirrorRestoreNewest(skipping:) (macOS): picks the
newest snapshot via listSnapshots(), skips it when it matches the
one already loaded (by name AND time, so a re-written hand-named
file still counts as new), and restores through the same hardened
DataBackup.restore core as a manual restore, with the size cap
lifted. It deletes the pre-restore whoop-replaced-*.sqlite copy on
success, so a launch-time restore doesn't leave a full-size copy
every time. It doesn't wait on an undownloaded iCloud placeholder:
it starts the download and loads it next launch, so launch is
never held up by the network.
- StrandApp.init calls the launch step before the @StateObject
AppModel (and so the store) is created. The restore swaps the
SQLite file underneath.
- FolderBackup.backupNow refuses while mirroring (this covers the
daily catch-up). A Mac snapshot would otherwise become the newest
one it then mirrors, and its prune could delete the iPhone's
snapshots. Backup & Sync greys out auto-backup and "Back up now"
while the mirror is on.
- BLEManager: new mirrorModeRefuses(_:) helper (logged once per
launch), checked first in whoopConnectAllowed (standing connect,
system connect, restored connect) and in the user-initiated paths
that bypass that gate: connectCore, scanForWhoops, startScan, and
the didDiscover connect.
- SourceCoordinator.start() returns early while mirroring, so no
non-WHOOP live source (standard HR, FTMS, Huami, Oura) starts for
devices listed in the mirrored registry.
- Backup & Sync (macOS): a "Mirror my iPhone" card with setup steps,
the last launch's status (or "quit and reopen" when switched on
mid-session) and a Quit button. The toggle needs a folder to turn
on, but can always be turned off.
The CBCentralManager is still created (BLEManager force-unwraps it in
43 places), so macOS may still ask for Bluetooth permission once.
Answering "Don't Allow" is fine in mirror mode.
Tested on macOS (MacBook, ad-hoc signed Release build) with a .noopbak
made from a real iPhone database (53 WHOOP workouts, 27 sleeps):
- First launch with mirror on: the Mac store went from 0 to 53
workouts, 27 sleeps and the journal. Status "Loaded the iPhone
backup from ...", no whoop-replaced-* copy left behind, zero scan
or connect lines, and one "Mirror mode: connect-from-system
refused" log line.
- Second launch: "Up to date with ...", no restore, no scanning.
- For contrast, the same app's earlier launch, before this change
and with no strap paired, logged 73 automatic WHOOP scans.
- The iOS app (NOOPiOS Release) still builds and runs; mirror mode is
a no-op there.
Not yet tested: a backup arriving through a real iCloud Drive folder
picked on both devices.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the strap is buzzing for a ringing call, a strap double-tap stops
the buzzing for that call (the call itself keeps ringing on the phone).
A double-tap with no call buzzing does whatever it normally does.
- CallBuzzMonitor.dismissFromStrap(): marks the current call as
silenced (no more bursts for that call UUID even if it keeps
ringing; cleared when it stops ringing) and returns true so the tap
is consumed.
- AppModel.handleDoubleTap: the call alert claims the double-tap
first, ahead of the 1.2 s debounce, a Lift Log override and the
user's configured double-tap action.
Making one double-tap enough: on hardware the first double-tap seemed
to need a second try. Decoding the strap's event frames showed the strap
had sensed it (DOUBLE_TAP, strap timestamp ~2 s into the ring), but the
phone received it ~1.5 s late, because the first buzz's HAPTICS_FIRED
event had kicked a strap-event history sync (~4 s offload). Meanwhile
the next 3-pulse burst (~2 s) had already been sent and played on. Two
fixes:
- BLEManager.holdStrapEventSync: asked before a strap-pushed EVENT
kicks a sync. AppModel points it at CallBuzzMonitor.isAlerting, so
no offload starts while a call is buzzing. Held syncs aren't lost:
the next trigger or the 15-min tick runs one.
- Shorter bursts: 2 pulses (~1.3 s) every 2 s instead of 3 pulses
every 3 s, so at most ~1.3 s of an in-flight burst plays on after a
double-tap.
Ending the call: researched, not possible. The tap also sends
CallKit's CXEndCallAction for the ringing call and logs iOS's answer. On
an iPhone 16 Pro (iOS 26) for a phone call, iOS refused with
com.apple.CallKit.error.requesttransaction 1 (unknownCallProvider): an
app can only end calls its own CXProvider reported, and no public API
ends or declines another app's or a carrier call. The sanctioned
accessory route is ANCS's negative action, which needs the strap's
firmware to be the ANCS client. The request is kept (harmless, and the
log line documents the outcome).
Settings caption: "Double-tap the strap to stop the buzzing (the call
itself keeps ringing on your phone)."
Tested on an iPhone 16 Pro (iOS 26) + WHOOP 5/MG with phone calls: one
double-tap now stops the buzzing straight away. Before the sync hold
and shorter bursts it needed two. The strap log shows "Double-tap → stop
call buzz" and the CallKit refusal above.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What this PR does
Type of change
How it was tested
Checklist
swift testinPackages/<name>)android/(./gradlew testFullDebugUnitTest; after a branch switch add--no-build-cache --rerun-tasks, or Gradle may fail classes you never touched)StrandDesigntokens — no hardcoded colors, fonts, or spacingdocs/CONTRIBUTING.mdStrand.xcodeproj/) or any secrets/keystoresRelated issues