Sport Mode, part 3/5: G7SensorKit — watchOS target + radio acquisition fixes - #65
Draft
ps2 wants to merge 11 commits into
Draft
Sport Mode, part 3/5: G7SensorKit — watchOS target + radio acquisition fixes#65ps2 wants to merge 11 commits into
ps2 wants to merge 11 commits into
Conversation
Mirrors the LoopKit-watchOS pbxproj pattern (SDKROOT=watchos, SUPPORTED_PLATFORMS watchos/watchsimulator, TARGETED_DEVICE_FAMILY=4, APPLICATION_EXTENSION_API_ONLY, PRODUCT_NAME=G7SensorKit, WATCHOS_DEPLOYMENT_TARGET=8.0). Compiles the full core (23 sources: parsing/state/messages/Common + the CoreBluetooth passive-listener stack, which builds for watchOS with the two os(iOS) restoration guards). Excluded from the watch target: G7DeviceStatus.swift (imports LoopKitUI, iOS-only; consumed only by G7SensorKitUI). Links LoopKit.framework (resolved to the LoopKit-watchOS product by name, as OmnipodKit-watchOS does). Shared scheme added for the standalone compile gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- G7BluetoothManager.swift: #if os(iOS) around the two CoreBluetooth state-restoration sites (restore-identifier init option, willRestoreState). watchOS branch passes options: nil; zero behavior change on iOS. Same recipe as the M2 OmnipodKit/RileyLinkBLEKit guards. - G7Sensor.swift: activationDate internal -> public (one line) — the minimal transport-injection seam so an external transport (the watch's direct-BLE G7Client) can drive the stock G7SensorDelegate flow into G7CGMManager. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ependency hijack fix The stock iOS G7SensorKit framework target used SDKROOT = auto, which XCBuild treats as platform-specializable: the WatchApp Extension's name-based implicit dependency on G7SensorKit.framework then resolved to the iOS target instead of G7SensorKit-watchOS, dragging the iOS dependency closure's SwiftCharts package modulemap flags into the watchos build description (66x 'module map file ... GeneratedModuleMaps-watchos/SwiftCharts.modulemap not found'). Pin SDKROOT = iphoneos on the target's two configs — the shape LoopKit's iOS target already has, and zero effective change on iOS (SUPPORTED_PLATFORMS already pins iphoneos there). Full diagnosis: Loop/docs/M3_NOTES.md §7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08-10 held-pod-link finding (0 adoptions in 3 held-link windows, 2 adoptions within minutes of the 2 releases) was proven by toggle experiment but the mechanism stayed inferred: this layer logged only via os_log, invisible to the on-watch mirrored log the field analysis reads. G7RadioCensus.sink (public; watch wires it, phone stays os_log-only) now reports: - scan-start branch taken: retrieved-known / system-connected list (the literal D2W piggyback, trigger a — a timing lottery against D2W's ~10-20s windows) / scan armed - connection events (trigger b — the OS reporting D2W's own connects), ALWAYS logged - ad discoveries (trigger c), rate-limited to one line per peripheral per 30s — presence vs absence per window is the scan-starvation discriminator - didConnect with peripheral identity - at both unknownCharacteristic throw sites: the full discovered GATT inventory, which distinguishes partial-discovery-on-a-dropped-shared-link from wrong-GATT — the ambiguity that made three days of auth errors uninterpretable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit ec73922)
- G7RadioCensus publishes connectPendingSince / lastRideSignalAt (lock-protected) so the app can gate pod radio work on live G7 acquisition state - census lines at didFailToConnect + didDisconnect (the invisible failing half of the 23:31:52-59 churn cycle) - duplicate-connect guard in handleDiscoveredPeripheral (re-registered connection events re-fired CONNECT ~10/s during a failed ride) - scanAfterDelay single-flight (N failures scheduled N rescans) (cherry picked from commit 3a6e3f3)
…nnected The known-sensor branch retrieved the peripheral and issued a bare pending connect() with NO scan armed (the arm condition was activePeripheral == nil, and the branch makes it non-nil). A pending connect rides bluetoothd's own duty-cycled background scan, which against the sensor's 1-4s advertising burst per 300s window is a lottery: measured 2026-08-19/20, seven consecutive windows missed, 20-40 min outages ending only when the sensor escalated to its ~60s distress cadence (also measured -- the 'exact 300s grid' is the collected regime only). Both collectors (ours and D2W) healed simultaneously because whoever finally connects, the other rides via trigger (b). The condition is now activePeripheral?.state != .connected: a scan is armed for the whole pending phase and catches the FIRST burst. didConnect stops it via readied -> managerQueue_stopScanning, and handleDiscoveredPeripheral's #101 guard makes a discovery-during-pending a no-op, so it cannot churn. Crude proved the same lesson: scan is the primitive. Plus the H14 scan watchdog: one full sensor window (320s) with acquisition armed and NOTHING delivered is deafness, not bad luck -- recycle the scan and any stuck pending connect, and say so in the census. Correct under every theory of the deafness. Plus the Radio Lab gates: G7Lab.trigger.a/b/c read at use, shipped defaults unchanged.
Ported from the pure/SportMode line. The census already logged every peripheral name it saw, but only inside sentences, so "which sensors are actually in range" could only be recovered by parsing log strings. The host needs it as data to break the stranded-identity trap: when the persisted sensor is gone and a replacement is advertising beside it, nothing in the manager can notice — it learns a new sensor's ID only by talking to it, and it is busy failing authentication against a corpse. Fired from the two sites that hold a peripheral name: ad discovery and connection events. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From the pure line, where a grep for Code=11 returned zero while 34 connection-limit failures sat in the log as localizedDescription text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three (ride + events + scan) proven over weeks of field use; the UserDefaults reads are gone so a stale false from an old experiment can never silently disable a doorway. Accessors stay so call sites read as gates. Co-Authored-By: Claude Opus 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 is
The G7SensorKit slice of Jeremy Barnum's Sport Mode line (standalone watch). Part 3 of a
bottom-up landing: LoopKit (#612) → RileyLinkKit (#18) / G7SensorKit → OmnipodKit → Loop →
workspace pin bump.
Draft: design review wanted before this goes near
next-dev.Eleven commits, rebased onto current
next-dev(fb87aa9). One real conflict, resolved — seebelow.
The changes, in two groups
1. watchOS support (4 commits) — additive
G7SensorKit-watchOSframework target and its shared scheme.SDKROOT. Without it, an implicit-dependency hijack lets the watchOStarget satisfy an iOS dependency — worth a look, since it touches the existing iOS target.
2. G7 radio acquisition work (7 commits) — behavioral, iOS included
This is the half that deserves scrutiny, because it changes stock iOS G7 behavior:
acquisition bug with a real fix, and the most valuable commit here for non-watch users.
signal rather than prose, and adds
[domain#code]on connect failures.infinity.are now hardcoded on. Whether that is the right shape for upstream, or whether they want to stay
configurable, is a design call for this review.
Conflict resolved (please check this one)
G7BluetoothManager.initconflicted.next-dev(#63) had extracted central-manager creation intoa
makeCentralManager(queue:)factory seam so tests can substitute a manager without the staterestoration option; Jeremy's commit had inlined
#if os(iOS)guards at the same call site.I kept your factory and moved the platform distinction inside it:
This preserves both intents and keeps
G7CGMManagerTests.TestBluetoothManager's override ofmakeCentralManagerworking. It is the one place in this PR where the resulting code matchesneither side verbatim, so it is the thing to check hardest.
Everything from #63 — the suspected-session-end grace period, its persistence across termination,
and its tests — is carried forward untouched.
Verification
xcodebuild -workspace LoopWorkspace.xcworkspace -scheme LoopWorkspaceagainst stocknext-dev(stock LoopKit, this branch + the RileyLinkKit branch swapped in): BUILDSUCCEEDED.
-scheme G7SensorKit-watchOS -destination 'generic/platform=watchOS Simulator': BUILDSUCCEEDED.
next-devcommit plusthe conflict resolution above.
The G7SensorKit test suite has not been run yet.