Repository navigation
SportMode - a watch that starts without the phone tells it by Apple push - #2624
Draft
jeremybarnum wants to merge 13 commits into
Draft
jeremybarnum wants to merge 13 commits into
jeremybarnum wants to merge 13 commits into
Conversation
The watch needs the same types to run the same loop as the phone. They move unchanged from the Loop app to LoopCore: the algorithm input (StoredDataAlgorithmInput), the dosing decision record (BolusDosingDecision), LoopConstants, LoopError with its messages and translations, LoopWarning, SimpleInsulinDose, CarbEntryChange, the four store protocols, SettingsProvider, the DosingDecisionStore, PumpManagerError and TempBasalRecommendation extensions, and LoopDataManager's conversions of carb entries and glucose samples for the algorithm. The algorithm input gains a public initializer, because the Loop app still builds it until the next commit. Files that use them import LoopCore. Nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sport Mode runs Loop's loop on the watch. Rather than a second copy of LoopDataManager.loop() there, the loop moves into LoopCore, where both apps run it. - LoopCycle holds the body of loop(): the input checks, the algorithm run, the dosing decision and its record, and the enact. It asks its host only for what the device knows: the stores, the settings and overrides, the algorithm experiments, the dosing strategy, whether dosing is held and whether the loop is closed. LoopDataManager is the phone's host. - The manual bolus recommendation, cancelling the loop's temp basal, the display run and the remote recommendation go through the cycle too. - The pump-facing members DeviceDataManager had for delivery (the pump's state, rounding, the checks before a dose is enacted) become PumpManagerDeliveryDelegate in LoopCore; each device sends the checked dose its own way. - The dosing record of a bolus asked for on the watch comes from one builder, BolusDosingDecision.watchBolus, which WatchDataManager now calls. The phone behaves as before, and its tests pass. One difference: the watch bolus record takes every field from the display state read at the start, where before it read some fields before computing the recommendation and some after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lending the pod to the watch needs a protocol both apps share, and one place that decides, in each situation, which device loops, which stands aside and which asks. - LoopCore/PodLoan: the messages (the request and grant, the takeover, records, the hand-back offer and its acknowledgement, the take-back, a status question and its answer), how they travel (live when the watch is reachable, and queued as well where a message must arrive), and the loan's records. - LoanControl: the control decisions, one function per situation. Each is a row of the control document, Documentation/SportMode/Control.md, which sets out the situations, the principles behind each decision and the known limits. - The SPORT_MODE_ENABLED build setting, off by default. With it off, neither app runs any of this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The phone's side of a loan, in its own files, on the main actor like the phone's managers. - Start: the phone lends only when it may (no bolus being given, no command to the pod unconfirmed, settings complete). It releases the pump and sends a grant: the pump's exported configuration, the settings, the glucose alert settings and dosing strategy, and recent insulin, carbs, glucose and overrides. - During the loan it doses nothing, stores the watch's records once each as they arrive, a temp basal still running once it has ended, and checks the pod's delivered total against them. - The pod comes back at a hand-back, when the user takes it back, or when the watch goes quiet and does not answer. The phone imports the records, checks the insulin and books an estimate for any the records cannot explain; later records replace it. - A loan the watch started without the phone, from the copy of the grant the phone keeps on the watch for that: when the phone hears from the watch again (its records, a hand-back offer, or its answer to the phone's question) about a loan this phone never granted, it stands aside, holding its dosing and releasing the pump. If the loan began before this phone last took the pod back because the watch went quiet, the phone tells the watch to stop instead. If its pod driver keeps the pod because the phone's last command is unconfirmed, the phone reads the pod, which settles the command, and the driver lets go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Everything Sport Mode changes in the phone's stock files, together, so it can be read in one place. Each hook does nothing on a phone that never lends its pod, except the Predicted Glucose screen's redraw below. - DeviceDataManager: a pump report runs the loan's insulin check before it completes; Loop's diagnostic report includes the loan's state. - LoopDataManager: automatic dosing is held while the pod is lent. - AlertManager: the Loop Failure warnings stop while the pump is released. - GlucoseAlertManager and MealDetectionManager: while the watch raises this user's glucose alerts, the phone evaluates none, and starts its episodes afresh afterwards. - WatchDataManager: loan messages go to the loan controller; a bolus sent from the watch while the pod is lent is refused. - LoopAppManager: the wiring, and banners in the foreground for the loan's notices. - PredictionTableViewController: the Predicted Glucose screen redraws when insulin, carbs or the forecast change, as the status screen does. During a loan the watch's records arrive without a glucose reading, so the open screen would otherwise fall behind until the next one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until now the watch app has been a display for the phone's loop. Its screens show the summary the phone sends after each cycle (insulin and carbs on board, the forecast, when the loop last ran), with the watch's own glucose reading in place of the phone's when it is newer, and the app keeps glucose and carb stores only to draw the chart and the carb list. A watch that runs the loop itself, as Sport Mode's does in the next commit, needs its doses and dosing decisions in stores too, and its screens need to show that loop instead of the phone's summary. This commit prepares both, so that Sport Mode builds on the stock watch app rather than running beside it with stores and screens of its own. - The watch's LoopDataManager opens dose and dosing-decision stores, with the alert history and device log, beside its glucose and carb stores, in the watch's usual locations. In Sport Mode the loop and the screens use the same stores. - Every screen reads one display state, which until now was used for the glucose reading alone: the phone's summary, with the watch's newer reading in its place. Sport Mode, in the next commit, lays the watch's own loop over it. - A fix: the carb list asks the phone for its carbs when it opens and when the phone's summary changes. Since the carb list moved to SwiftUI, nothing has asked, so the list could stay empty. Without Sport Mode the code changes, but the watch app looks and works as before, apart from that fix to the carb list. The simulator tests check the watch app with Sport Mode built in and with it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the previous commits the watch can run Loop's loop cycle on stores of its own, but it has no pump to dose with: the pod belongs to the phone. This commit adds the watch's side of a loan, in its own folder, on the main actor. - WatchLoopManager runs the shared loop cycle on the watch, over the pump the phone lends. Its cycle work runs one step at a time, in the order it was asked for, as on the phone. - PodLoanWatchController handles the loan from the watch's side: Start, with the phone or, when the phone can't be reached, from the copy of the grant the phone keeps on the watch; the takeover of the pod; records sent to the phone as checkpoints during the loan; End and the hand-back; standing aside when the phone takes the pod back; and resuming after the watch app relaunches, which checks with the phone first and counts any insulin given while the watch was off before it doses again. - During a loan the watch raises the glucose alarms (with Loop's own GlucoseAlertManager, compiled for the watch) and the pod and Loop Failure alerts. It keeps its dosing decisions, alert history and device log, which go to the phone in one file when the loan ends. - Hooks in stock watch files: the extension delegate builds the Sport Mode stack and holds loan messages that arrive before it is ready; LoopDataManager hands each stored glucose reading to the loop, and shows the loop's state and override on the screens during a loan; WatchCGMHost reports unreliable sensor data, so the loop cancels a high temp basal, as the phone does. - The watch's entitlements come from a build setting, empty by default, so without Sport Mode the watch signs as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the previous commits a loan runs end to end, but nothing on either screen starts one, ends one or says one is running: the watch's pages and the phone's status screen look as they do without Sport Mode. - Watch: the Sport Mode glance, a page after the watch app's usual two, which shows glucose and trend in the user's unit, the loop ring, insulin and carbs on board and the running temp basal, with Start and End. The watch moves to it when a loan starts, when the app relaunches into a loan, and after carbs or a bolus are entered during a loan; outside a loan the app opens where it always has. - Watch: during a loan, carbs can be deleted from the carb list. A carb entry drives the dose, so the user needs a way to correct a wrong one while the watch has the pod; deleting gives that now, and editing is left for later. A bolus or an End that is refused says why on the screen. With Sport Mode built in, the check that the phone is set up applies page by page, so the glance stays reachable when it is not. - Phone: a "Sport Mode Active" banner, which says when the watch's records last arrived, and a pod tile that say the watch has the pod and offer to take it back; carbs, bolus, presets and settings unavailable while it does; the loop's status ring stops ageing, and its explanation says the watch is running the loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oan the watch started without the phone Follows up the review point that, during Sport Mode, the phone's screens leave out what the watch has done, even when the two are in touch. The earlier commit answered it by saving the watch's records on the phone as they arrive and showing on the banner how current they are. It missed two things: - The banner's time moved whenever the watch said anything, even when it sent no records. So the banner could say the phone was more current than it was. Now it moves only when the watch's records are saved on the phone, and reads "Insulin and carbs as of …". - If the watch started Sport Mode while the phone was away, the phone ignored the watch's records even after the two were back in touch, until Sport Mode ended. Its insulin and carbs stayed as they were when it left, and the banner showed no time. Now, once they are in touch, the phone takes the watch's records and saves them as they arrive, as it does when Sport Mode starts with the phone nearby. For this, the watch includes the phone's offline-start credential with its records. Until the first records arrive, the banner shows when the phone last read the pod itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… by Apple push When the watch starts Sport Mode while the phone is away, the phone doesn't know until the two are back in touch. Until then it keeps trying to loop and keeps sending "Loop not looping" notifications, which reach the watch that is looping. If the watch has its own internet, it now sends the phone one silent Apple push right after taking the pod, and the phone stands aside as it would on hearing from the watch in person. The push is signed with an Apple push key the user saves in the phone's settings, which reaches the watch with the standing copy. It is behind the build flag SPORT_MODE_PUSH_ENABLED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of it We're narrowing down which Apple Watch generations work with Sport Mode: an SE and a Series 9 do, a Series 5 doesn't. On a Series 7 the grant never reached the watch while smaller messages got through, and the phone's log couldn't say why: the grant goes urgently and queued at once, and a failed urgent copy wasn't logged. The phone now logs the grant's size and whether it went urgently or queued only, and a failed urgent send of any message that also rides the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Sport Mode: a watch that starts Sport Mode without the phone tells it by Apple push. Stacked on #2545; only the last commit is this PR (net diff). From my fork so you can push edits. Behind the build flag
SPORT_MODE_PUSH_ENABLED; with it off, nothing changes.The problem. When Sport Mode starts with the phone nearby, the phone knows the watch has the pod. It stops looping, and its "Loop not looping" notifications are cancelled. When the watch starts Sport Mode while the phone is away, the phone doesn't know until the two are back in touch. Until then it keeps trying to loop and keeps firing "not looping" notifications. Those are mirrored to the watch by iOS, which reaches a paired watch over Bluetooth, a shared Wi-Fi network, or, with a cellular watch, the internet. So the watch, which is looping, is told repeatedly that Loop isn't. That's distracting. The phone also still believes it holds the pod, which is a dual-control risk.
What this does. If the watch has its own internet (cellular, or Wi-Fi it knows), it sends the phone one silent Apple push right after it takes the pod. The push wakes the phone, and it stands aside exactly as it would on hearing from the watch in person: it stops looping, holds its dosing, and its notifications stop. Nothing is shown; the phone and watch log it. There is one attempt. If the push is lost, the existing path still applies: the phone stands aside when it next hears from the watch.
The credential. Apple push needs an APNs key from the Apple Developer team the app is built with, the same kind of key Nightscout uses for Loop's remote commands. The user pastes it into Settings ▸ Sport Mode ▸ Push Key on the phone. It's kept in the phone's keychain and goes to the watch inside the standing copy the phone already sends, with the phone's push address. The team ID comes from the build. No key is built into the app, and no server is involved. Each push carries the phone's offline-start credential, so the phone acts only on a push for a Sport Mode start it allowed, and ignores one older than 10 minutes.
Setup. Users will need instructions. Creating the key is the same as for Nightscout remote commands: LoopDocs, Apple Push Notifications. Without a key, nothing changes.
Follow-up, not in this PR. The obvious next step is sending the watch's insulin and carb records by push, so a distant phone stays current, which would mitigate risk. Once dosing data travels by push, though, a lost, late or repeated delivery becomes a dosing question, so that's left for its own decision.
Stock code touched:
LoopAppManager, 3 lines: a Sport Mode push is handled before the services see it.SettingsView, 3 lines: the Push Key row.FeatureFlags.Info.plist: the team ID.🤖 Generated with Claude Code