From 54630a7286c68e8a8ebdcc30f607ef0e803e1b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 10:59:08 +0200 Subject: [PATCH 01/25] Add an Apple Watch app fed by the phone, with remote commands for Loop and Trio The phone sends glucose, chart data and remote settings to the watch over WatchConnectivity; the watch holds no credentials. Commands from the watch are validated and sent by the phone through the existing Loop APNS and Trio Remote Control paths, gated by a new Remote setting. Includes WidgetKit complications, watch targets in fastlane and tests for the watch protocol. --- LoopFollow.xcodeproj/project.pbxproj | 342 ++++++++++++++++++ LoopFollow/Application/AppDelegate.swift | 2 + LoopFollow/Charts/BGChartModel.swift | 1 + .../LiveActivity/LiveActivityManager.swift | 4 +- .../Remote/Settings/RemoteSettingsView.swift | 8 + LoopFollow/Storage/Storage.swift | 2 + LoopFollow/Watch/WatchCommandHandler.swift | 275 ++++++++++++++ LoopFollow/Watch/WatchSessionManager.swift | 138 +++++++ LoopFollow/Watch/WatchStateBuilder.swift | 158 ++++++++ .../AppIcon.appiconset/1024.png | Bin 0 -> 477801 bytes .../AppIcon.appiconset/Contents.json | 14 + LoopFollowWatch/Assets.xcassets/Contents.json | 6 + LoopFollowWatch/ContentView.swift | 43 +++ LoopFollowWatch/GlucosePage.swift | 115 ++++++ LoopFollowWatch/Info.plist | 28 ++ LoopFollowWatch/InfoPage.swift | 94 +++++ LoopFollowWatch/LoopFollowWatch.entitlements | 10 + LoopFollowWatch/LoopFollowWatchApp.swift | 29 ++ LoopFollowWatch/Remote/BolusEntryView.swift | 127 +++++++ .../Remote/CommandConfirmView.swift | 163 +++++++++ LoopFollowWatch/Remote/MealEntryView.swift | 99 +++++ LoopFollowWatch/Remote/PresetListView.swift | 79 ++++ LoopFollowWatch/Remote/RemotePage.swift | 76 ++++ LoopFollowWatch/WatchChartView.swift | 153 ++++++++ LoopFollowWatch/WatchSession.swift | 116 ++++++ .../GlucoseComplicationView.swift | 132 +++++++ LoopFollowWatchWidgets/Info.plist | 29 ++ .../LoopFollowWatchWidgets.entitlements | 10 + .../LoopFollowWatchWidgets.swift | 50 +++ Shared/AppGroupID.swift | 2 + Tests/WatchProtocolTests.swift | 86 +++++ WatchShared/WatchCommand.swift | 45 +++ WatchShared/WatchFormat.swift | 104 ++++++ WatchShared/WatchState.swift | 112 ++++++ WatchShared/WatchStateStore.swift | 26 ++ fastlane/Fastfile | 36 +- 36 files changed, 2709 insertions(+), 5 deletions(-) create mode 100644 LoopFollow/Watch/WatchCommandHandler.swift create mode 100644 LoopFollow/Watch/WatchSessionManager.swift create mode 100644 LoopFollow/Watch/WatchStateBuilder.swift create mode 100644 LoopFollowWatch/Assets.xcassets/AppIcon.appiconset/1024.png create mode 100644 LoopFollowWatch/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 LoopFollowWatch/Assets.xcassets/Contents.json create mode 100644 LoopFollowWatch/ContentView.swift create mode 100644 LoopFollowWatch/GlucosePage.swift create mode 100644 LoopFollowWatch/Info.plist create mode 100644 LoopFollowWatch/InfoPage.swift create mode 100644 LoopFollowWatch/LoopFollowWatch.entitlements create mode 100644 LoopFollowWatch/LoopFollowWatchApp.swift create mode 100644 LoopFollowWatch/Remote/BolusEntryView.swift create mode 100644 LoopFollowWatch/Remote/CommandConfirmView.swift create mode 100644 LoopFollowWatch/Remote/MealEntryView.swift create mode 100644 LoopFollowWatch/Remote/PresetListView.swift create mode 100644 LoopFollowWatch/Remote/RemotePage.swift create mode 100644 LoopFollowWatch/WatchChartView.swift create mode 100644 LoopFollowWatch/WatchSession.swift create mode 100644 LoopFollowWatchWidgets/GlucoseComplicationView.swift create mode 100644 LoopFollowWatchWidgets/Info.plist create mode 100644 LoopFollowWatchWidgets/LoopFollowWatchWidgets.entitlements create mode 100644 LoopFollowWatchWidgets/LoopFollowWatchWidgets.swift create mode 100644 Tests/WatchProtocolTests.swift create mode 100644 WatchShared/WatchCommand.swift create mode 100644 WatchShared/WatchFormat.swift create mode 100644 WatchShared/WatchState.swift create mode 100644 WatchShared/WatchStateStore.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 560cd2f8e..c9c5d2091 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ DDF2C0142BEFD468007A20E6 /* blacklisted-versions.json in Resources */ = {isa = PBXBuildFile; fileRef = DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */; }; FC5A5C3D2497B229009C550E /* Config.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = FC5A5C3C2497B229009C550E /* Config.xcconfig */; }; FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; }; + DD57A7C02F70000000000001 /* LoopFollowWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = DD57A7C02F70000000000002 /* LoopFollowWatch.app */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DD57A7C02F70000000000003 /* LoopFollowWatchWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DD57A7C02F70000000000004 /* LoopFollowWatchWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,6 +36,20 @@ remoteGlobalIDString = FC9788132485969B00A7906C; remoteInfo = LoopFollow; }; + DD57A7C02F70000000000005 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FC97880C2485969B00A7906C /* Project object */; + proxyType = 1; + remoteGlobalIDString = DD57A7C02F70000000000006; + remoteInfo = LoopFollowWatch; + }; + DD57A7C02F70000000000007 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FC97880C2485969B00A7906C /* Project object */; + proxyType = 1; + remoteGlobalIDString = DD57A7C02F70000000000008; + remoteInfo = LoopFollowWatchWidgets; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,6 +64,28 @@ name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + DD57A7C02F70000000000009 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + DD57A7C02F70000000000001 /* LoopFollowWatch.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; + DD57A7C02F7000000000000A /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + DD57A7C02F70000000000003 /* LoopFollowWatchWidgets.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -68,6 +106,8 @@ FC9788142485969B00A7906C /* Loop Follow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Loop Follow.app"; sourceTree = BUILT_PRODUCTS_DIR; }; FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; FCFEEC9D2486E68E00402A7F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + DD57A7C02F70000000000002 /* LoopFollowWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LoopFollowWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DD57A7C02F70000000000004 /* LoopFollowWatchWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LoopFollowWatchWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -78,6 +118,42 @@ ); target = FC9788132485969B00A7906C /* LoopFollow */; }; + DD57A7C02F7000000000000B /* Exceptions for "LoopFollowWatch" folder in "LoopFollowWatch" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + LoopFollowWatch.entitlements, + ); + target = DD57A7C02F70000000000006 /* LoopFollowWatch */; + }; + DD57A7C02F7000000000000C /* Exceptions for "LoopFollowWatchWidgets" folder in "LoopFollowWatchWidgets" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + LoopFollowWatchWidgets.entitlements, + ); + target = DD57A7C02F70000000000008 /* LoopFollowWatchWidgets */; + }; + DD57A7C02F7000000000000D /* Exceptions for "Shared" folder in "LoopFollowWatch" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + GlucoseLiveActivityAttributes.swift, + LAAppGroupSettings.swift, + LALivenessMarker.swift, + LALivenessStore.swift, + ); + target = DD57A7C02F70000000000006 /* LoopFollowWatch */; + }; + DD57A7C02F7000000000000E /* Exceptions for "Shared" folder in "LoopFollowWatchWidgets" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + GlucoseLiveActivityAttributes.swift, + LAAppGroupSettings.swift, + LALivenessMarker.swift, + LALivenessStore.swift, + ); + target = DD57A7C02F70000000000008 /* LoopFollowWatchWidgets */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -103,6 +179,8 @@ BC0FBFD3754955D028EF6631 /* Shared */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( + DD57A7C02F7000000000000D /* Exceptions for "Shared" folder in "LoopFollowWatch" target */, + DD57A7C02F7000000000000E /* Exceptions for "Shared" folder in "LoopFollowWatchWidgets" target */, ); path = Shared; sourceTree = ""; @@ -118,6 +196,29 @@ path = Tests; sourceTree = ""; }; + DD57A7C02F7000000000000F /* LoopFollowWatch */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + DD57A7C02F7000000000000B /* Exceptions */, + ); + path = LoopFollowWatch; + sourceTree = ""; + }; + DD57A7C02F70000000000010 /* LoopFollowWatchWidgets */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + DD57A7C02F7000000000000C /* Exceptions */, + ); + path = LoopFollowWatchWidgets; + sourceTree = ""; + }; + DD57A7C02F70000000000011 /* WatchShared */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = WatchShared; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -147,6 +248,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DD57A7C02F70000000000012 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DD57A7C02F70000000000013 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -196,6 +311,9 @@ 6A5880E0B811AF443B05AB02 /* Frameworks */, 9555FE45F7FAEFA6142D04CA /* LoopFollow */, BC0FBFD3754955D028EF6631 /* Shared */, + DD57A7C02F7000000000000F /* LoopFollowWatch */, + DD57A7C02F70000000000010 /* LoopFollowWatchWidgets */, + DD57A7C02F70000000000011 /* WatchShared */, ); sourceTree = ""; }; @@ -205,6 +323,8 @@ FC9788142485969B00A7906C /* Loop Follow.app */, DDCC3AD62DDE1790006F1C10 /* Tests.xctest */, 37A4BDD92F5B6B4A00EEB289 /* LoopFollowLAExtensionExtension.appex */, + DD57A7C02F70000000000002 /* LoopFollowWatch.app */, + DD57A7C02F70000000000004 /* LoopFollowWatchWidgets.appex */, ); name = Products; sourceTree = ""; @@ -266,15 +386,18 @@ 04DA71CCA0280FA5FA2DF7A6 /* [CP] Embed Pods Frameworks */, DDB0AF532BB1AA0900AFA48B /* Capture Build Details */, 37A4BDED2F5B6B4C00EEB289 /* Embed Foundation Extensions */, + DD57A7C02F70000000000009 /* Embed Watch Content */, ); buildRules = ( ); dependencies = ( 37A4BDE72F5B6B4C00EEB289 /* PBXTargetDependency */, + DD57A7C02F7000000000001B /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 9555FE45F7FAEFA6142D04CA /* LoopFollow */, BC0FBFD3754955D028EF6631 /* Shared */, + DD57A7C02F70000000000011 /* WatchShared */, ); name = LoopFollow; packageProductDependencies = ( @@ -284,6 +407,52 @@ productReference = FC9788142485969B00A7906C /* Loop Follow.app */; productType = "com.apple.product-type.application"; }; + DD57A7C02F70000000000006 /* LoopFollowWatch */ = { + isa = PBXNativeTarget; + buildConfigurationList = DD57A7C02F70000000000014 /* Build configuration list for PBXNativeTarget "LoopFollowWatch" */; + buildPhases = ( + DD57A7C02F70000000000015 /* Sources */, + DD57A7C02F70000000000012 /* Frameworks */, + DD57A7C02F70000000000016 /* Resources */, + DD57A7C02F7000000000000A /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + DD57A7C02F70000000000017 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + DD57A7C02F7000000000000F /* LoopFollowWatch */, + DD57A7C02F70000000000011 /* WatchShared */, + BC0FBFD3754955D028EF6631 /* Shared */, + ); + name = LoopFollowWatch; + productName = LoopFollowWatch; + productReference = DD57A7C02F70000000000002 /* LoopFollowWatch.app */; + productType = "com.apple.product-type.application"; + }; + DD57A7C02F70000000000008 /* LoopFollowWatchWidgets */ = { + isa = PBXNativeTarget; + buildConfigurationList = DD57A7C02F70000000000018 /* Build configuration list for PBXNativeTarget "LoopFollowWatchWidgets" */; + buildPhases = ( + DD57A7C02F70000000000019 /* Sources */, + DD57A7C02F70000000000013 /* Frameworks */, + DD57A7C02F7000000000001A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + DD57A7C02F70000000000010 /* LoopFollowWatchWidgets */, + DD57A7C02F70000000000011 /* WatchShared */, + BC0FBFD3754955D028EF6631 /* Shared */, + ); + name = LoopFollowWatchWidgets; + productName = LoopFollowWatchWidgets; + productReference = DD57A7C02F70000000000004 /* LoopFollowWatchWidgets.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -297,6 +466,12 @@ 37A4BDD82F5B6B4A00EEB289 = { CreatedOnToolsVersion = 26.2; }; + DD57A7C02F70000000000006 = { + CreatedOnToolsVersion = 27.0; + }; + DD57A7C02F70000000000008 = { + CreatedOnToolsVersion = 27.0; + }; DDCC3AD52DDE1790006F1C10 = { CreatedOnToolsVersion = 16.3; TestTargetID = FC9788132485969B00A7906C; @@ -325,6 +500,8 @@ FC9788132485969B00A7906C /* LoopFollow */, DDCC3AD52DDE1790006F1C10 /* Tests */, 37A4BDD82F5B6B4A00EEB289 /* LoopFollowLAExtensionExtension */, + DD57A7C02F70000000000006 /* LoopFollowWatch */, + DD57A7C02F70000000000008 /* LoopFollowWatchWidgets */, ); }; /* End PBXProject section */ @@ -355,6 +532,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DD57A7C02F70000000000016 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DD57A7C02F7000000000001A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -462,6 +653,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DD57A7C02F70000000000015 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DD57A7C02F70000000000019 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -476,6 +681,17 @@ target = FC9788132485969B00A7906C /* LoopFollow */; targetProxy = DDCC3ADA2DDE1790006F1C10 /* PBXContainerItemProxy */; }; + DD57A7C02F7000000000001B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = DD57A7C02F70000000000006 /* LoopFollowWatch */; + targetProxy = DD57A7C02F70000000000005 /* PBXContainerItemProxy */; + }; + DD57A7C02F70000000000017 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DD57A7C02F70000000000008 /* LoopFollowWatchWidgets */; + targetProxy = DD57A7C02F70000000000007 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -813,6 +1029,114 @@ }; name = Release; }; + DD57A7C02F7000000000001C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = LoopFollowWatch/LoopFollowWatch.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = LoopFollowWatch/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.$(unique_id).LoopFollow$(app_suffix).watchkitapp"; + PRODUCT_MODULE_NAME = LoopFollowWatch; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; + DD57A7C02F7000000000001D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = LoopFollowWatch/LoopFollowWatch.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = LoopFollowWatch/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.$(unique_id).LoopFollow$(app_suffix).watchkitapp"; + PRODUCT_MODULE_NAME = LoopFollowWatch; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Release; + }; + DD57A7C02F7000000000001E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LoopFollowWatchWidgets/LoopFollowWatchWidgets.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = LoopFollowWatchWidgets/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.$(unique_id).LoopFollow$(app_suffix).watchkitapp.widgets"; + PRODUCT_MODULE_NAME = LoopFollowWatchWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; + DD57A7C02F7000000000001F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LoopFollowWatchWidgets/LoopFollowWatchWidgets.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = LoopFollowWatchWidgets/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.$(unique_id).LoopFollow$(app_suffix).watchkitapp.widgets"; + PRODUCT_MODULE_NAME = LoopFollowWatchWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -852,6 +1176,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DD57A7C02F70000000000014 /* Build configuration list for PBXNativeTarget "LoopFollowWatch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD57A7C02F7000000000001C /* Debug */, + DD57A7C02F7000000000001D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DD57A7C02F70000000000018 /* Build configuration list for PBXNativeTarget "LoopFollowWatchWidgets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD57A7C02F7000000000001E /* Debug */, + DD57A7C02F7000000000001F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index 3cc65b736..d7b7f9735 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -55,6 +55,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UNUserNotificationCenter.current().delegate = self + WatchSessionManager.shared.activate() + // Only spin up Bluetooth if the user has chosen a BLE-based background // refresh. Initializing BLEManager creates a CBCentralManager, which // triggers the Bluetooth permission prompt — deferring it keeps that diff --git a/LoopFollow/Charts/BGChartModel.swift b/LoopFollow/Charts/BGChartModel.swift index ab6deeb67..d23d22e29 100644 --- a/LoopFollow/Charts/BGChartModel.swift +++ b/LoopFollow/Charts/BGChartModel.swift @@ -403,6 +403,7 @@ final class BGChartModel: ObservableObject { guard let self else { return } self.rebuildScheduled = false self.performRebuild() + WatchSessionManager.shared.requestUpdate(reason: "chart") } } diff --git a/LoopFollow/LiveActivity/LiveActivityManager.swift b/LoopFollow/LiveActivity/LiveActivityManager.swift index 82987e79b..0d3e61fe5 100644 --- a/LoopFollow/LiveActivity/LiveActivityManager.swift +++ b/LoopFollow/LiveActivity/LiveActivityManager.swift @@ -659,6 +659,7 @@ final class LiveActivityManager { DispatchQueue.main.async { self.refreshFromCurrentState(reason: reason) } return } + WatchSessionManager.shared.requestUpdate(reason: reason) refreshCoalescer.request(reason: reason) } @@ -1064,10 +1065,9 @@ final class LiveActivityManager { let snapshotUnchanged = GlucoseSnapshotStore.shared.load() == snapshot let thresholdsChanged = LAAppGroupSettings.thresholdsMgdl() != UnitSettingsStore.shared.effectiveThresholds() - // Store + Watch: always update, independent of LA state. + // Store: always update, independent of LA state. writeThresholds() GlucoseSnapshotStore.shared.save(snapshot) - // WatchConnectivityManager.shared.send(snapshot: snapshot) // LA update: gated on LA being active, snapshot having changed, and activities enabled. if !Storage.shared.laEnabled.value { diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index c4f8a5c92..c3146108c 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -9,6 +9,7 @@ struct RemoteSettingsView: View { @ObservedObject var viewModel: RemoteSettingsViewModel @ObservedObject private var device = Storage.shared.device @ObservedObject var bolusIncrement = Storage.shared.bolusIncrement + @ObservedObject private var watchRemoteCommands = Storage.shared.watchRemoteCommands @State private var showAlert: Bool = false @State private var alertType: AlertType? = nil @@ -93,6 +94,13 @@ struct RemoteSettingsView: View { if viewModel.remoteType == .trc || viewModel.remoteType == .loopAPNS { guardrailsSection + + Section( + header: Text("Apple Watch"), + footer: Text("Commands from the LoopFollow watch app are sent by this iPhone, with the same guardrails as here. The iPhone must be within reach of the watch.") + ) { + Toggle("Allow Commands from Apple Watch", isOn: $watchRemoteCommands.value) + } } if !Storage.shared.bolusIncrementDetected.value { diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 1432352b7..1cd80fb64 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -34,6 +34,8 @@ class Storage { var mealWithBolus = StorageValue(key: "mealWithBolus", defaultValue: false) var mealWithFatProtein = StorageValue(key: "mealWithFatProtein", defaultValue: false) + /// Lets the Apple Watch app send remote commands through this phone. + var watchRemoteCommands = StorageValue(key: "watchRemoteCommands", defaultValue: false) // TODO: This flag can be deleted in March 2027. Check the commit for other places to cleanup. var hasSeenFatProteinOrderChange = StorageValue(key: "hasSeenFatProteinOrderChange", defaultValue: true) diff --git a/LoopFollow/Watch/WatchCommandHandler.swift b/LoopFollow/Watch/WatchCommandHandler.swift new file mode 100644 index 000000000..c861677a5 --- /dev/null +++ b/LoopFollow/Watch/WatchCommandHandler.swift @@ -0,0 +1,275 @@ +// LoopFollow +// WatchCommandHandler.swift + +import Foundation +import HealthKit + +/// Executes commands from the watch with the same checks and send paths as the +/// phone's Remote screens. Runs on the main queue. +final class WatchCommandHandler { + static let shared = WatchCommandHandler() + + /// Loop rejects carb entries outside this window; same limits as LoopAPNSCarbsView. + private let loopMaxPastHours: Double = 12 + private let loopMaxFutureHours: Double = 1 + /// Loop's default absorption time for a remote carb entry, in hours. + private let loopAbsorptionHours = 3.0 + + private var handledIDs: [UUID] = [] + + private init() {} + + func handle(_ command: WatchCommand, reply: @escaping (WatchCommandReply) -> Void) { + let finish: (Bool, String) -> Void = { success, message in + LogManager.shared.log(category: .apns, message: "Watch command \(Self.name(of: command.action)) finished: success=\(success) \(message)") + reply(WatchCommandReply(success: success, message: message)) + } + + guard !handledIDs.contains(command.id) else { + finish(false, "This command was already handled.") + return + } + handledIDs.append(command.id) + if handledIDs.count > 50 { handledIDs.removeFirst() } + + guard abs(Date().timeIntervalSince(command.createdAt)) <= WatchCommand.maxAge else { + finish(false, "The command arrived too late and was not sent. Try again.") + return + } + + LogManager.shared.log(category: .apns, message: "Watch command received: \(Self.name(of: command.action))") + + if case .snoozeAlarm = command.action { + guard Observable.shared.currentAlarm.value != nil else { + finish(false, "No alarm is active.") + return + } + AlarmManager.shared.performSnooze() + finish(true, "Alarm snoozed.") + return + } + + guard Storage.shared.watchRemoteCommands.value else { + finish(false, "Commands from Apple Watch are turned off in LoopFollow's Remote settings.") + return + } + + switch WatchStateBuilder.remoteKind() { + case .loop: + handleLoop(command.action, finish: finish) + case .trio: + handleTrio(command.action, finish: finish) + case nil: + finish(false, "Remote control is not set up in LoopFollow.") + } + } + + // MARK: - Loop + + private func handleLoop(_ action: WatchCommand.Action, finish: @escaping (Bool, String) -> Void) { + let service = LoopAPNSService() + let sent: (Bool, String?) -> Void = { success, error in + DispatchQueue.main.async { + finish(success, success ? RemoteCommandMessage.sent : (error ?? "Failed to send the command.")) + } + } + + switch action { + case let .bolus(units): + guard units > 0 else { return finish(false, "Enter an amount above zero.") } + guard units <= maxBolus else { return finish(false, exceedsMaxBolus(units)) } + guard service.validateSetup() else { return finish(false, loopSetupIncomplete) } + guard let otp = loopOTP(finish: finish) else { return } + service.sendBolusViaAPNS(payload: LoopAPNSPayload(type: .bolus, bolusAmount: units, otp: otp)) { success, error in + DispatchQueue.main.async { + if success { + TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value) + QuickPickBolusesManager.shared.recordBolus(units: units) + } + } + sent(success, error) + } + + case let .meal(carbs, fat, protein, bolus, consumedAt): + guard fat == 0, protein == 0, bolus == 0 else { return finish(false, "Loop accepts carbs only from the watch.") } + guard carbs > 0 else { return finish(false, "Enter an amount above zero.") } + guard carbs <= maxCarbs else { return finish(false, exceeds("Carbs", carbs, maxCarbs)) } + let now = Date() + guard consumedAt >= now.addingTimeInterval(-loopMaxPastHours * 3600) else { + return finish(false, "Time must be within the prior \(Int(loopMaxPastHours)) hours.") + } + guard consumedAt <= now.addingTimeInterval(loopMaxFutureHours * 3600) else { + return finish(false, "Time must be within the next \(Int(loopMaxFutureHours)) hour.") + } + guard service.validateSetup() else { return finish(false, loopSetupIncomplete) } + guard let otp = loopOTP(finish: finish) else { return } + let payload = LoopAPNSPayload( + type: .carbs, + carbsAmount: carbs, + absorptionTime: loopAbsorptionHours, + consumedDate: consumedAt.dateUsingCurrentSeconds(), + otp: otp + ) + service.sendCarbsViaAPNS(payload: payload) { success, error in + DispatchQueue.main.async { + if success { + TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value) + QuickPickMealsManager.shared.recordMeal(carbs: carbs) + } + } + sent(success, error) + } + + case let .startOverride(presetID): + guard let preset = ProfileManager.shared.loopOverrides.first(where: { $0.name == presetID }) else { + return finish(false, "The override preset was not found.") + } + let duration = TimeInterval(preset.duration ?? 0) + service.sendOverrideNotification(presetName: preset.name, duration: duration > 0 ? duration : nil, completion: sent) + + case .cancelOverride: + service.sendCancelOverrideNotification(completion: sent) + + case .startTempTarget, .cancelTempTarget: + finish(false, "Temp targets are not available for Loop.") + + case .snoozeAlarm: + break + } + } + + private var loopSetupIncomplete: String { + "Loop APNS setup is incomplete. Configure it in LoopFollow's Remote settings." + } + + /// Mirrors the one-time code checks in the phone's Loop bolus and carbs screens. + private func loopOTP(finish: (Bool, String) -> Void) -> String? { + let url = Storage.shared.loopAPNSQrCodeURL.value + guard let otp = TOTPGenerator.extractOTPFromURL(url) else { + finish(false, "Invalid QR code URL. Re-scan the QR code in LoopFollow's Remote settings.") + return nil + } + guard !TOTPService.shared.isTOTPBlocked(qrCodeURL: url) else { + finish(false, "The current one-time code was already used. Wait for the next code (up to 30 seconds) and try again.") + return nil + } + return otp + } + + // MARK: - Trio + + private func handleTrio(_ action: WatchCommand.Action, finish: @escaping (Bool, String) -> Void) { + let manager = PushNotificationManager() + let sent: (Bool, String?) -> Void = { success, error in + DispatchQueue.main.async { + finish(success, success ? RemoteCommandMessage.sent : (error ?? "Failed to send the command.")) + } + } + + switch action { + case let .bolus(units): + let stepped = roundedToBolusStep(units) + guard stepped > 0 else { return finish(false, "Enter an amount above zero.") } + guard stepped <= maxBolus else { return finish(false, exceedsMaxBolus(stepped)) } + manager.sendBolusPushNotification(bolusAmount: HKQuantity(unit: .internationalUnit(), doubleValue: stepped)) { success, error in + if success { + DispatchQueue.main.async { QuickPickBolusesManager.shared.recordBolus(units: stepped) } + } + sent(success, error) + } + + case let .meal(carbs, fat, protein, bolus, consumedAt): + let withFatProtein = Storage.shared.mealWithFatProtein.value + let withBolus = Storage.shared.mealWithBolus.value + guard withFatProtein || (fat == 0 && protein == 0) else { return finish(false, "Meal with Fat/Protein is turned off in LoopFollow's Remote settings.") } + guard withBolus || bolus == 0 else { return finish(false, "Meal with Bolus is turned off in LoopFollow's Remote settings.") } + guard carbs >= 0, fat >= 0, protein >= 0, carbs > 0 || fat > 0 || protein > 0 else { + return finish(false, "Enter carbs, fat or protein.") + } + guard carbs <= maxCarbs else { return finish(false, exceeds("Carbs", carbs, maxCarbs)) } + guard fat <= Storage.shared.maxFat.value.doubleValue(for: .gram()) else { + return finish(false, exceeds("Fat", fat, Storage.shared.maxFat.value.doubleValue(for: .gram()))) + } + guard protein <= Storage.shared.maxProtein.value.doubleValue(for: .gram()) else { + return finish(false, exceeds("Protein", protein, Storage.shared.maxProtein.value.doubleValue(for: .gram()))) + } + let steppedBolus = roundedToBolusStep(bolus) + guard steppedBolus <= maxBolus else { return finish(false, exceedsMaxBolus(steppedBolus)) } + + // Within a minute of now counts as "now", as in the phone's meal screen. + let scheduledTime: Date? = abs(consumedAt.timeIntervalSinceNow) < 60 ? nil : consumedAt.dateUsingCurrentSeconds() + manager.sendMealPushNotification( + carbs: HKQuantity(unit: .gram(), doubleValue: carbs), + protein: HKQuantity(unit: .gram(), doubleValue: protein), + fat: HKQuantity(unit: .gram(), doubleValue: fat), + bolusAmount: HKQuantity(unit: .internationalUnit(), doubleValue: steppedBolus), + scheduledTime: scheduledTime + ) { success, error in + if success, carbs > 0 { + DispatchQueue.main.async { + QuickPickMealsManager.shared.recordMeal(carbs: carbs, fat: fat, protein: protein, bolus: steppedBolus) + } + } + sent(success, error) + } + + case let .startOverride(presetID): + guard let override = ProfileManager.shared.trioOverrides.first(where: { $0.name == presetID }) else { + return finish(false, "The override preset was not found.") + } + manager.sendOverridePushNotification(override: override, completion: sent) + + case .cancelOverride: + manager.sendCancelOverridePushNotification(completion: sent) + + case let .startTempTarget(presetID): + guard let preset = TempTargetPresetManager.shared.presets.first(where: { $0.id.uuidString == presetID }) else { + return finish(false, "The temp target preset was not found.") + } + manager.sendTempTargetPushNotification(target: preset.target, duration: preset.duration, completion: sent) + + case .cancelTempTarget: + manager.sendCancelTempTargetPushNotification(completion: sent) + + case .snoozeAlarm: + break + } + } + + // MARK: - Helpers + + private var maxBolus: Double { + Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()) + } + + private var maxCarbs: Double { + Storage.shared.maxCarbs.value.doubleValue(for: .gram()) + } + + /// Rounds down to the bolus increment, as the phone's Trio bolus screen does. + private func roundedToBolusStep(_ value: Double) -> Double { + let step = max(0.001, Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit())) + let stepped = ((value / step) + 1e-10).rounded(.down) * step + return (stepped * 1000).rounded() / 1000 + } + + private func exceedsMaxBolus(_ units: Double) -> String { + "\(InsulinFormatter.shared.string(units)) U exceeds the maximum bolus of \(InsulinFormatter.shared.string(maxBolus)) U set in LoopFollow." + } + + private func exceeds(_ what: String, _ grams: Double, _ max: Double) -> String { + "\(what) (\(Int(grams)) g) exceeds the maximum of \(Int(max)) g set in LoopFollow." + } + + private static func name(of action: WatchCommand.Action) -> String { + switch action { + case .bolus: "bolus" + case .meal: "meal" + case .startOverride: "startOverride" + case .cancelOverride: "cancelOverride" + case .startTempTarget: "startTempTarget" + case .cancelTempTarget: "cancelTempTarget" + case .snoozeAlarm: "snoozeAlarm" + } + } +} diff --git a/LoopFollow/Watch/WatchSessionManager.swift b/LoopFollow/Watch/WatchSessionManager.swift new file mode 100644 index 000000000..dc841e40e --- /dev/null +++ b/LoopFollow/Watch/WatchSessionManager.swift @@ -0,0 +1,138 @@ +// LoopFollow +// WatchSessionManager.swift + +import Combine +import Foundation +import WatchConnectivity + +/// Phone side of the Apple Watch link. Publishes `WatchState` as the +/// application context and relays commands from the watch to +/// `WatchCommandHandler`. +final class WatchSessionManager: NSObject { + static let shared = WatchSessionManager() + + /// Complication pushes are rationed by watchOS (about 50 per day), so they + /// are only used when a new reading arrives and this long has passed. + private let complicationTransferInterval: TimeInterval = 15 * 60 + + private var lastSentState: WatchState? + private var lastComplicationTransfer: Date = .distantPast + private var cancellables = Set() + private lazy var coalescer = RefreshCoalescer(delay: 2) { [weak self] reason in + self?.publish(reason: reason) + } + + private var session: WCSession? { + WCSession.isSupported() ? WCSession.default : nil + } + + func activate() { + guard let session else { return } + session.delegate = self + session.activate() + observeSettings() + } + + /// Schedules a state update. Safe to call often; requests are coalesced. + func requestUpdate(reason: String) { + guard Thread.isMainThread else { + DispatchQueue.main.async { self.requestUpdate(reason: reason) } + return + } + guard let session, session.activationState == .activated, session.isPaired, session.isWatchAppInstalled else { return } + coalescer.request(reason: reason) + } + + private func observeSettings() { + let triggers: [AnyPublisher] = [ + Observable.shared.currentAlarm.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.watchRemoteCommands.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.remoteType.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.colorBGText.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.units.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.mealWithBolus.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.mealWithFatProtein.$value.map { _ in () }.eraseToAnyPublisher(), + ] + Publishers.MergeMany(triggers) + .dropFirst(triggers.count) + .sink { [weak self] in self?.requestUpdate(reason: "settings") } + .store(in: &cancellables) + } + + private func publish(reason: String) { + guard let session, session.activationState == .activated else { return } + let state = WatchStateBuilder.build() + guard let data = state.encoded() else { return } + + do { + try session.updateApplicationContext([WatchState.contextKey: data]) + } catch { + LogManager.shared.log(category: .general, message: "[Watch] context update failed: \(error.localizedDescription)") + } + + let newReading = state.snapshot?.updatedAt != lastSentState?.snapshot?.updatedAt + if newReading, + session.isComplicationEnabled, + session.remainingComplicationUserInfoTransfers > 0, + Date().timeIntervalSince(lastComplicationTransfer) >= complicationTransferInterval + { + session.transferCurrentComplicationUserInfo([WatchState.contextKey: data]) + lastComplicationTransfer = Date() + } + + lastSentState = state + LogManager.shared.log(category: .general, message: "[Watch] state sent (\(data.count) bytes) reason=\(reason)", isDebug: true) + } +} + +// MARK: - WCSessionDelegate + +extension WatchSessionManager: WCSessionDelegate { + func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error: Error?) { + if let error { + LogManager.shared.log(category: .general, message: "[Watch] activation failed: \(error.localizedDescription)") + return + } + if state == .activated { + requestUpdate(reason: "activated") + } + } + + func sessionDidBecomeInactive(_: WCSession) {} + + func sessionDidDeactivate(_ session: WCSession) { + // Required when the user switches to another watch. + session.activate() + } + + func sessionWatchStateDidChange(_: WCSession) { + requestUpdate(reason: "watchStateChanged") + } + + func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { + DispatchQueue.main.async { + if message[WatchMessageKey.requestState] != nil { + let data = WatchStateBuilder.build().encoded() ?? Data() + replyHandler([WatchState.contextKey: data]) + return + } + + guard let data = message[WatchCommand.messageKey] as? Data, + let command = try? JSONDecoder().decode(WatchCommand.self, from: data) + else { + Self.reply(WatchCommandReply(success: false, message: "Unknown request."), to: replyHandler) + return + } + + WatchCommandHandler.shared.handle(command) { result in + Self.reply(result, to: replyHandler) + self.requestUpdate(reason: "command") + } + } + } + + private static func reply(_ reply: WatchCommandReply, to handler: ([String: Any]) -> Void) { + let data = (try? JSONEncoder().encode(reply)) ?? Data() + handler([WatchCommandReply.messageKey: data]) + } +} diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift new file mode 100644 index 000000000..359602522 --- /dev/null +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -0,0 +1,158 @@ +// LoopFollow +// WatchStateBuilder.swift + +import Foundation +import HealthKit + +/// Collects the phone's current data into the state the watch renders. +enum WatchStateBuilder { + /// History sent to the watch; the watch chart scrolls back this far. + static let historyWindow: TimeInterval = 24 * 3600 + + static func build(now: Date = Date()) -> WatchState { + let thresholds = UnitSettingsStore.shared.effectiveThresholds() + let vc = MainViewController.shared + let cutoff = now.timeIntervalSince1970 - historyWindow + + let readings = (vc?.bgData ?? []) + .filter { $0.date >= cutoff && $0.sgv > 0 } + .map { WatchReading(t: $0.date, v: Double($0.sgv)) } + + let boluses = ((vc?.bolusData ?? []) + (vc?.smbData ?? [])) + .filter { $0.date >= cutoff } + .map { WatchReading(t: $0.date, v: $0.value) } + .sorted { $0.t < $1.t } + + let carbs = (vc?.carbData ?? []) + .filter { $0.date >= cutoff } + .map { WatchReading(t: $0.date, v: $0.value) } + + var predictions: [WatchPredictionLine] = [] + if let vc { + let lines: [(WatchPredictionLine.Kind, [ShareGlucoseData])] = [ + (.loop, vc.predictionData), + (.zt, vc.ztPredictionData), + (.iob, vc.iobPredictionData), + (.cob, vc.cobPredictionData), + (.uam, vc.uamPredictionData), + ] + for (kind, data) in lines where !data.isEmpty { + predictions.append(WatchPredictionLine(kind: kind, points: data.map { WatchReading(t: $0.date, v: Double($0.sgv)) })) + } + } + + let cone = (vc?.chartModel.cone ?? []).map { + WatchConePoint(t: $0.date.timeIntervalSince1970, low: $0.yMin, high: $0.yMax) + } + + let lastLoop = Observable.shared.alertLastLoopTime.value.flatMap { $0 > 0 ? Date(timeIntervalSince1970: $0) : nil } + + return WatchState( + builtAt: now, + snapshot: GlucoseSnapshotBuilder.build(from: StorageCurrentGlucoseStateProvider()), + readings: readings, + predictions: predictions, + cone: cone, + boluses: boluses, + carbs: carbs, + lowMgdl: thresholds.low, + highMgdl: thresholds.high, + colorBGText: Storage.shared.colorBGText.value, + lastLoopAt: lastLoop, + device: Storage.shared.device.value, + alarm: currentAlarm(), + remote: remoteConfig() + ) + } + + private static func currentAlarm() -> WatchAlarm? { + guard let id = Observable.shared.currentAlarm.value, + let alarm = Storage.shared.alarms.value.first(where: { $0.id == id }) + else { return nil } + return WatchAlarm(id: id, name: alarm.name) + } + + // MARK: - Remote + + static func remoteKind() -> WatchRemoteConfig.Kind? { + switch Storage.shared.remoteType.value { + case .loopAPNS: + return Storage.shared.device.value == "Loop" ? .loop : nil + case .trc: + return Storage.shared.device.value == "Trio" ? .trio : nil + case .none: + return nil + } + } + + private static func remoteConfig() -> WatchRemoteConfig? { + guard Storage.shared.watchRemoteCommands.value, let kind = remoteKind() else { return nil } + + let recommendedAt: TimeInterval? = kind == .trio + ? Observable.shared.enactedOrSuggested.value + : Observable.shared.alertLastLoopTime.value + + return WatchRemoteConfig( + kind: kind, + maxBolus: Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()), + bolusIncrement: max(0.001, Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit())), + maxCarbs: Storage.shared.maxCarbs.value.doubleValue(for: .gram()), + maxFat: Storage.shared.maxFat.value.doubleValue(for: .gram()), + maxProtein: Storage.shared.maxProtein.value.doubleValue(for: .gram()), + mealWithBolus: kind == .trio && Storage.shared.mealWithBolus.value, + mealWithFatProtein: kind == .trio && Storage.shared.mealWithFatProtein.value, + recommendedBolus: Observable.shared.deviceRecBolus.value, + recommendedBolusAt: recommendedAt.map { Date(timeIntervalSince1970: $0) }, + overridePresets: kind == .trio ? trioOverridePresets() : loopOverridePresets(), + tempTargetPresets: kind == .trio ? tempTargetPresets() : [] + ) + } + + private static func loopOverridePresets() -> [WatchPreset] { + ProfileManager.shared.loopOverrides.map { override in + var parts: [String] = [] + if override.insulinNeedsScaleFactor != 1 { + parts.append("\(Int(round(override.insulinNeedsScaleFactor * 100))) %") + } + if override.targetRange.count >= 2 { + let low = Localizer.formatQuantity(override.targetRange[0]) + let high = Localizer.formatQuantity(override.targetRange[1]) + parts.append(low == high ? low : "\(low)–\(high)") + } + parts.append(durationText(seconds: TimeInterval(override.duration ?? 0))) + let symbol = override.symbol.isEmpty ? "" : "\(override.symbol) " + return WatchPreset(id: override.name, name: symbol + override.name, detail: parts.joined(separator: ", ")) + } + } + + private static func trioOverridePresets() -> [WatchPreset] { + ProfileManager.shared.trioOverrides.map { override in + var parts: [String] = [] + if let percentage = override.percentage { + parts.append("\(Int(percentage)) %") + } + if let target = override.target { + parts.append(Localizer.formatQuantity(target)) + } + parts.append(durationText(seconds: (override.duration ?? 0) * 60)) + return WatchPreset(id: override.name, name: override.name, detail: parts.joined(separator: ", ")) + } + } + + private static func tempTargetPresets() -> [WatchPreset] { + TempTargetPresetManager.shared.presets.map { preset in + let target = Localizer.formatQuantity(preset.target) + let duration = durationText(seconds: preset.duration.doubleValue(for: .second())) + return WatchPreset(id: preset.id.uuidString, name: preset.name, detail: "\(target), \(duration)") + } + } + + private static func durationText(seconds: TimeInterval) -> String { + guard seconds > 0 else { return "Indefinite" } + let hours = Int(seconds) / 3600 + let minutes = (Int(seconds) % 3600) / 60 + if hours > 0, minutes > 0 { return "\(hours) h \(minutes) min" } + if hours > 0 { return "\(hours) h" } + return "\(minutes) min" + } +} diff --git a/LoopFollowWatch/Assets.xcassets/AppIcon.appiconset/1024.png b/LoopFollowWatch/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000000000000000000000000000000000000..a975ee0639e82b0a49a7272371b6ac3dc2c2d5d9 GIT binary patch literal 477801 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

?YpwzU%nq_g%*?zyCTOe*7d32HR{#vWJ5>)4@(i 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΄re>kh6IpgoMJ)wd`9)Yb!go8G0M;5^ozfam0 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 WatchFormat.staleAfter ? .red : .secondary) + } + } + .navigationBarTitleDisplayMode(.inline) + } + + @ViewBuilder + private func header(now: Date) -> some View { + if let snapshot = state.snapshot { + let isStale = now.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter + HStack(alignment: .center, spacing: 6) { + Text(WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit)) + .font(.system(size: 40, weight: .black)) + .foregroundStyle(WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText)) + .strikethrough(isStale, pattern: .solid, color: .red) + .lineLimit(1) + .minimumScaleFactor(0.5) + VStack(alignment: .leading, spacing: 0) { + Text(WatchFormat.trendArrow(snapshot.trend)) + .font(.system(size: 22, weight: .black)) + Text(WatchFormat.delta(snapshot.delta, unit: snapshot.unit)) + .font(.system(size: 15)) + .foregroundStyle(.secondary) + } + .lineLimit(1) + Spacer(minLength: 0) + } + if snapshot.isNotLooping { + Label("Not looping", systemImage: "exclamationmark.triangle.fill") + .font(.footnote) + .foregroundStyle(.red) + } + } else { + Text("No BG data") + .font(.headline) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private var footer: some View { + if let snapshot = state.snapshot, snapshot.iob != nil || snapshot.cob != nil { + HStack { + Text("IOB \(WatchFormat.iob(snapshot.iob))") + Spacer() + Text("COB \(WatchFormat.cob(snapshot.cob))") + } + .font(.footnote) + .lineLimit(1) + .minimumScaleFactor(0.7) + } + } +} + +/// Shown while an alarm is sounding on the phone. +struct AlarmBanner: View { + let alarm: WatchAlarm + @EnvironmentObject private var session: WatchSession + @State private var isSending = false + @State private var error: String? + + var body: some View { + Button { + Task { + isSending = true + let reply = await session.send(.snoozeAlarm) + isSending = false + error = reply.success ? nil : reply.message + } + } label: { + HStack { + Image(systemName: "bell.fill") + VStack(alignment: .leading) { + Text(alarm.name) + .lineLimit(1) + Text(isSending ? "Snoozing…" : "Tap to snooze") + .font(.caption2) + .opacity(0.8) + } + Spacer(minLength: 0) + } + } + .buttonStyle(.borderedProminent) + .tint(.red) + .disabled(isSending) + .alert("Snooze failed", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) { + Button("OK", role: .cancel) {} + } message: { + Text(error ?? "") + } + } +} diff --git a/LoopFollowWatch/Info.plist b/LoopFollowWatch/Info.plist new file mode 100644 index 000000000..cdb0808fa --- /dev/null +++ b/LoopFollowWatch/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(display_name) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(LOOP_FOLLOW_MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + WKApplication + + WKCompanionAppBundleIdentifier + com.$(unique_id).LoopFollow$(app_suffix) + + diff --git a/LoopFollowWatch/InfoPage.swift b/LoopFollowWatch/InfoPage.swift new file mode 100644 index 000000000..f18b32962 --- /dev/null +++ b/LoopFollowWatch/InfoPage.swift @@ -0,0 +1,94 @@ +// LoopFollow +// InfoPage.swift + +import SwiftUI + +/// A compact table like the phone's info table; rows without data are hidden. +struct InfoPage: View { + let state: WatchState + + var body: some View { + ScrollView { + VStack(spacing: 0) { + rows + } + } + .navigationTitle("Info") + } + + @ViewBuilder + private var rows: some View { + if let s = state.snapshot { + row("IOB", s.iob.map { WatchFormat.iob($0) }) + row("COB", s.cob.map { WatchFormat.cob($0) }) + row("Rec. Bolus", s.recBolus.map { "\(WatchFormat.insulin($0)) U" }) + row("Basal", s.basalRate.isEmpty ? nil : s.basalRate) + row("Projected", s.projected.map { WatchFormat.glucose($0, unit: s.unit) }) + row("Min/Max", minMax(s)) + row("Override", s.override.map { name in name + remaining(s.overrideEndAt) }) + row("Temp Target", s.tempTargetMgdl.map { WatchFormat.glucose($0, unit: s.unit) + remaining(s.tempTargetEndAt) }) + row("Target", target(s)) + row("ISF", s.isfMgdlPerU.map { WatchFormat.glucose($0, unit: s.unit) }) + row("CR", s.carbRatio.map { "\(WatchFormat.insulin($0)) g" }) + row("Autosens", s.autosens.map { "\(Int(round($0 * 100))) %" }) + row("TDD", s.tdd.map { "\(WatchFormat.insulin($0)) U" }) + row("Carbs today", s.carbsToday.map { "\(Int(round($0))) g" }) + row("Pump", s.pumpReservoirU.map { "\(WatchFormat.insulin($0)) U" }) + row("Pump Battery", s.pumpBattery.map { "\(Int(round($0))) %" }) + row("Battery", s.battery.map { "\(Int(round($0))) %" }) + row("SAGE", age(s.sageInsertTime)) + row("CAGE", age(s.cageInsertTime)) + row("IAGE", age(s.iageInsertTime)) + row("Profile", s.profileName) + } + row("Last Loop", state.lastLoopAt.map { WatchFormat.minutesAgo($0) + " ago" }) + if let sensor = state.snapshot?.cgmSensorState { + Text(sensor) + .font(.footnote) + .foregroundStyle(.orange) + .padding(.top, 4) + } + } + + @ViewBuilder + private func row(_ label: String, _ value: String?) -> some View { + if let value { + HStack { + Text(label) + .foregroundStyle(.secondary) + Spacer() + Text(value) + .multilineTextAlignment(.trailing) + } + .font(.footnote) + .padding(.vertical, 3) + Divider() + } + } + + private func minMax(_ s: GlucoseSnapshot) -> String? { + guard let low = s.minBgMgdl, let high = s.maxBgMgdl else { return nil } + return "\(WatchFormat.glucose(low, unit: s.unit))/\(WatchFormat.glucose(high, unit: s.unit))" + } + + private func target(_ s: GlucoseSnapshot) -> String? { + guard let low = s.targetLowMgdl else { return nil } + let lowText = WatchFormat.glucose(low, unit: s.unit) + guard let high = s.targetHighMgdl, round(high) != round(low) else { return lowText } + return "\(lowText)–\(WatchFormat.glucose(high, unit: s.unit))" + } + + private func remaining(_ endAt: TimeInterval?) -> String { + guard let endAt else { return "" } + let minutes = Int((endAt - Date().timeIntervalSince1970) / 60) + guard minutes > 0 else { return "" } + return minutes >= 60 ? " (\(minutes / 60) h \(minutes % 60) min)" : " (\(minutes) min)" + } + + private func age(_ insertTime: TimeInterval) -> String? { + guard insertTime > 0 else { return nil } + let hours = Int((Date().timeIntervalSince1970 - insertTime) / 3600) + guard hours >= 0 else { return nil } + return "\(hours / 24)d \(hours % 24)h" + } +} diff --git a/LoopFollowWatch/LoopFollowWatch.entitlements b/LoopFollowWatch/LoopFollowWatch.entitlements new file mode 100644 index 000000000..5b963cc90 --- /dev/null +++ b/LoopFollowWatch/LoopFollowWatch.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.$(unique_id).LoopFollow$(app_suffix) + + + diff --git a/LoopFollowWatch/LoopFollowWatchApp.swift b/LoopFollowWatch/LoopFollowWatchApp.swift new file mode 100644 index 000000000..595ce0b9a --- /dev/null +++ b/LoopFollowWatch/LoopFollowWatchApp.swift @@ -0,0 +1,29 @@ +// LoopFollow +// LoopFollowWatchApp.swift + +import SwiftUI + +@main +struct LoopFollowWatchApp: App { + @StateObject private var session = WatchSession.shared + @Environment(\.scenePhase) private var scenePhase + + init() { + WatchSession.shared.activate() + } + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(session) + } + .onChange(of: scenePhase) { _, phase in + if phase == .active { + session.requestState() + } + } + .backgroundTask(.watchConnectivity) { + await WatchSession.shared.waitForPendingContent() + } + } +} diff --git a/LoopFollowWatch/Remote/BolusEntryView.swift b/LoopFollowWatch/Remote/BolusEntryView.swift new file mode 100644 index 000000000..157fd2276 --- /dev/null +++ b/LoopFollowWatch/Remote/BolusEntryView.swift @@ -0,0 +1,127 @@ +// LoopFollow +// BolusEntryView.swift + +import SwiftUI + +struct BolusEntryView: View { + let state: WatchState + let remote: WatchRemoteConfig + + @Environment(\.dismiss) private var dismiss + @State private var units = 0.0 + @State private var staleRecommendation: Double? + @FocusState private var amountFocused: Bool + + /// Same cut-offs as the phone's bolus screens. + private let recommendationMaxAge: TimeInterval = 12 * 60 + private let recommendationWarnAge: TimeInterval = 5 * 60 + + private var step: Double { max(remote.bolusIncrement, 0.025) } + + var body: some View { + ScrollView { + VStack(spacing: 8) { + AmountField(value: $units, unit: "U", step: step, maximum: remote.maxBolus, isHighlighted: amountFocused, format: WatchFormat.insulin) + .focused($amountFocused) + Text("Max \(WatchFormat.insulin(remote.maxBolus)) U") + .font(.footnote) + .foregroundStyle(.secondary) + + recommendation + + NavigationLink { + CommandConfirmView( + title: "Bolus", + lines: ["\(WatchFormat.insulin(units)) U"], + action: .bolus(units: units), + deliversInsulin: true, + onSuccess: { dismiss() } + ) + } label: { + Text("Continue") + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .disabled(units <= 0) + } + } + .navigationTitle("Bolus") + .onAppear { amountFocused = true } + .alert( + "Old Calculation Warning", + isPresented: Binding(get: { staleRecommendation != nil }, set: { if !$0 { staleRecommendation = nil } }) + ) { + Button("Use Anyway") { + if let staleRecommendation { units = staleRecommendation } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("This recommended bolus was calculated more than 5 minutes ago. New treatments may have occurred since then. Proceed with caution.") + } + } + + @ViewBuilder + private var recommendation: some View { + if let rec = remote.recommendedBolus, rec > 0, let at = remote.recommendedBolusAt { + let age = Date().timeIntervalSince(at) + if age < recommendationMaxAge { + let stepped = Self.roundedDown(min(rec, remote.maxBolus), step: step) + Button { + if age >= recommendationWarnAge { + staleRecommendation = stepped + } else { + units = stepped + } + } label: { + VStack(alignment: .leading, spacing: 2) { + Text("Recommended \(WatchFormat.insulin(stepped)) U") + Text("Calculated \(Int(age / 60)) min ago") + .font(.caption2) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + Text("New treatments may have occurred since the recommended bolus was calculated.") + .font(.caption2) + .foregroundStyle(age >= recommendationWarnAge ? .red : .yellow) + } + } + } + + static func roundedDown(_ value: Double, step: Double) -> Double { + let stepped = ((value / step) + 1e-10).rounded(.down) * step + return (stepped * 1000).rounded() / 1000 + } +} + +/// A value edited with the Digital Crown, in fixed steps between a minimum and a maximum. +struct AmountField: View { + @Binding var value: Double + let unit: String + let step: Double + var minimum: Double = 0 + let maximum: Double + /// The caller owns focus; this only draws the highlight. + let isHighlighted: Bool + let format: (Double) -> String + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(format(value)) + .font(.system(size: 34, weight: .bold)) + .monospacedDigit() + Text(unit) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + .background(RoundedRectangle(cornerRadius: 8).stroke(isHighlighted ? Color.green : Color.gray.opacity(0.4), lineWidth: 1.5)) + .focusable() + .digitalCrownRotation($value, from: minimum, through: max(maximum, minimum + step), by: step, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) + .onChange(of: value) { _, newValue in + let nearest = ((newValue / step).rounded() * step * 1000).rounded() / 1000 + let clamped = min(max(minimum, nearest), maximum) + if clamped != newValue { value = clamped } + } + } +} diff --git a/LoopFollowWatch/Remote/CommandConfirmView.swift b/LoopFollowWatch/Remote/CommandConfirmView.swift new file mode 100644 index 000000000..da224b1ea --- /dev/null +++ b/LoopFollowWatch/Remote/CommandConfirmView.swift @@ -0,0 +1,163 @@ +// LoopFollow +// CommandConfirmView.swift + +import LocalAuthentication +import SwiftUI +import WatchKit + +/// Final step for every command: shows what will be sent, asks for confirmation +/// and shows the phone's reply. Insulin needs a full turn of the Digital Crown +/// and an unlocked watch on the wrist. +struct CommandConfirmView: View { + let title: String + let lines: [String] + let action: WatchCommand.Action + let deliversInsulin: Bool + /// Called when the user closes the result of a successful send. + var onSuccess: () -> Void = {} + + @EnvironmentObject private var session: WatchSession + @Environment(\.dismiss) private var dismiss + + @State private var phase: Phase = .confirm + @State private var crownProgress = 0.0 + @State private var lastCrownChange = Date() + @FocusState private var crownFocused: Bool + + enum Phase: Equatable { + case confirm + case sending + case done(success: Bool, message: String) + } + + var body: some View { + ScrollView { + VStack(spacing: 8) { + switch phase { + case .confirm: + summary + if deliversInsulin { + crownConfirm + } else { + Button("Send") { Task { await send() } } + .buttonStyle(.borderedProminent) + .tint(.blue) + } + case .sending: + ProgressView() + Text("Sending…") + .foregroundStyle(.secondary) + case let .done(success, message): + Image(systemName: success ? "checkmark.circle.fill" : "xmark.octagon.fill") + .font(.largeTitle) + .foregroundStyle(success ? .green : .red) + Text(message) + .font(.footnote) + .multilineTextAlignment(.center) + Button("Done") { + dismiss() + if success { onSuccess() } + } + } + } + .frame(maxWidth: .infinity) + } + .navigationTitle(title) + .navigationBarBackButtonHidden(phase == .sending) + } + + private var summary: some View { + VStack(spacing: 2) { + ForEach(lines, id: \.self) { line in + Text(line) + .font(.headline) + .multilineTextAlignment(.center) + } + } + } + + private var crownConfirm: some View { + VStack(spacing: 4) { + ZStack { + Circle() + .stroke(Color.gray.opacity(0.3), lineWidth: 6) + Circle() + .trim(from: 0, to: crownProgress) + .stroke(Color.blue, style: StrokeStyle(lineWidth: 6, lineCap: .round)) + .rotationEffect(.degrees(-90)) + Image(systemName: "digitalcrown.arrow.clockwise") + .font(.title3) + } + .frame(width: 56, height: 56) + Text("Turn the Digital Crown to confirm") + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .focusable() + .focused($crownFocused) + .digitalCrownRotation($crownProgress, from: 0, through: 1, by: 0.02, sensitivity: .low, isContinuous: false, isHapticFeedbackEnabled: true) + .onAppear { crownFocused = true } + .onChange(of: crownProgress) { _, value in + lastCrownChange = Date() + if value >= 1 { + WKInterfaceDevice.current().play(.success) + Task { await authenticateAndSend() } + } else { + resetCrownIfIdle() + } + } + } + + /// Partial turns reset after a pause, so a bump of the crown never adds up to a confirmation. + private func resetCrownIfIdle() { + let changedAt = lastCrownChange + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + if phase == .confirm, lastCrownChange == changedAt, crownProgress < 1 { + withAnimation { crownProgress = 0 } + } + } + } + + private func authenticateAndSend() async { + switch await Self.authenticate() { + case .success: + await send() + case let .failure(message): + crownProgress = 0 + phase = .done(success: false, message: message) + WKInterfaceDevice.current().play(.failure) + } + } + + private func send() async { + phase = .sending + let reply = await session.send(action) + phase = .done(success: reply.success, message: reply.message) + WKInterfaceDevice.current().play(reply.success ? .success : .failure) + } + + private enum AuthOutcome { + case success + case failure(String) + } + + /// Requires a passcode-protected watch that is unlocked on the wrist. + private static func authenticate() async -> AuthOutcome { + #if targetEnvironment(simulator) + return .success + #else + let context = LAContext() + var error: NSError? + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithWristDetection, error: &error) else { + return .failure("Insulin can only be sent from a watch with a passcode and Wrist Detection turned on.") + } + do { + let ok = try await context.evaluatePolicy(.deviceOwnerAuthenticationWithWristDetection, localizedReason: "Confirm to send insulin") + return ok ? .success : .failure("Authentication failed.") + } catch { + return .failure("Authentication failed.") + } + #endif + } +} diff --git a/LoopFollowWatch/Remote/MealEntryView.swift b/LoopFollowWatch/Remote/MealEntryView.swift new file mode 100644 index 000000000..6b78681fa --- /dev/null +++ b/LoopFollowWatch/Remote/MealEntryView.swift @@ -0,0 +1,99 @@ +// LoopFollow +// MealEntryView.swift + +import SwiftUI + +/// Carbs for Loop; carbs, fat, protein and an optional bolus for Trio, following +/// the phone's Meal settings. Everything goes to the phone as one command. +struct MealEntryView: View { + let remote: WatchRemoteConfig + + @Environment(\.dismiss) private var dismiss + @State private var carbs = 0.0 + @State private var fat = 0.0 + @State private var protein = 0.0 + @State private var bolus = 0.0 + @State private var offsetMinutes = 0.0 + @FocusState private var focus: Field? + + private enum Field: Hashable { + case carbs, fat, protein, bolus, time + } + + private var bolusStep: Double { max(remote.bolusIncrement, 0.025) } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 6) { + field("Carbs", .carbs) { + AmountField(value: $carbs, unit: "g", step: 1, maximum: remote.maxCarbs, isHighlighted: focus == .carbs, format: grams) + } + if remote.mealWithFatProtein { + field("Fat", .fat) { + AmountField(value: $fat, unit: "g", step: 1, maximum: remote.maxFat, isHighlighted: focus == .fat, format: grams) + } + field("Protein", .protein) { + AmountField(value: $protein, unit: "g", step: 1, maximum: remote.maxProtein, isHighlighted: focus == .protein, format: grams) + } + } + if remote.mealWithBolus { + field("Bolus", .bolus) { + AmountField(value: $bolus, unit: "U", step: bolusStep, maximum: remote.maxBolus, isHighlighted: focus == .bolus, format: WatchFormat.insulin) + } + } + field("Time", .time) { + AmountField(value: $offsetMinutes, unit: "min", step: 5, minimum: -120, maximum: 60, isHighlighted: focus == .time, format: timeText) + } + + NavigationLink { + CommandConfirmView( + title: "Meal", + lines: summaryLines, + action: .meal(carbs: carbs, fat: fat, protein: protein, bolus: bolus, consumedAt: Date().addingTimeInterval(offsetMinutes * 60)), + deliversInsulin: bolus > 0, + onSuccess: { dismiss() } + ) + } label: { + Text("Continue") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .disabled(carbs <= 0 && fat <= 0 && protein <= 0) + } + } + .navigationTitle("Meal") + .onAppear { focus = .carbs } + } + + private func field(_ label: String, _ id: Field, content: () -> AmountField) -> some View { + VStack(alignment: .leading, spacing: 2) { + Text(label) + .font(.footnote) + .foregroundStyle(.secondary) + content() + .focused($focus, equals: id) + } + } + + private func grams(_ value: Double) -> String { + "\(Int(value))" + } + + private func timeText(_ minutes: Double) -> String { + let m = Int(minutes) + if m == 0 { return "Now" } + return m > 0 ? "+\(m)" : "\(m)" + } + + private var summaryLines: [String] { + var lines: [String] = [] + if carbs > 0 { lines.append("Carbs \(Int(carbs)) g") } + if fat > 0 { lines.append("Fat \(Int(fat)) g") } + if protein > 0 { lines.append("Protein \(Int(protein)) g") } + if bolus > 0 { lines.append("Bolus \(WatchFormat.insulin(bolus)) U") } + let at = Date().addingTimeInterval(offsetMinutes * 60) + lines.append(offsetMinutes == 0 ? "Now" : at.formatted(date: .omitted, time: .shortened)) + return lines + } +} diff --git a/LoopFollowWatch/Remote/PresetListView.swift b/LoopFollowWatch/Remote/PresetListView.swift new file mode 100644 index 000000000..5eb2a8dee --- /dev/null +++ b/LoopFollowWatch/Remote/PresetListView.swift @@ -0,0 +1,79 @@ +// LoopFollow +// PresetListView.swift + +import SwiftUI + +/// Override or temp target presets synced from the phone, with a cancel row +/// while one is active. +struct PresetListView: View { + enum Kind { + case override + case tempTarget + } + + let kind: Kind + let state: WatchState + let presets: [WatchPreset] + + @Environment(\.dismiss) private var dismiss + + private var activeText: String? { + guard let snapshot = state.snapshot else { return nil } + switch kind { + case .override: + return snapshot.override + case .tempTarget: + return snapshot.tempTargetMgdl.map { WatchFormat.glucose($0, unit: snapshot.unit) } + } + } + + private var title: String { kind == .override ? "Overrides" : "Temp Target" } + + var body: some View { + List { + if let activeText { + Section("Active") { + NavigationLink { + CommandConfirmView( + title: kind == .override ? "Cancel Override" : "Cancel Temp Target", + lines: ["Cancel \(activeText)?"], + action: kind == .override ? .cancelOverride : .cancelTempTarget, + deliversInsulin: false, + onSuccess: { dismiss() } + ) + } label: { + Label(activeText, systemImage: "xmark.circle") + .foregroundStyle(.red) + } + } + } + + Section("Presets") { + if presets.isEmpty { + Text(kind == .override ? "No override presets found." : "No temp target presets. Add them in LoopFollow on the iPhone.") + .font(.footnote) + .foregroundStyle(.secondary) + } + ForEach(presets) { preset in + NavigationLink { + CommandConfirmView( + title: title, + lines: [preset.name, preset.detail], + action: kind == .override ? .startOverride(presetID: preset.id) : .startTempTarget(presetID: preset.id), + deliversInsulin: false, + onSuccess: { dismiss() } + ) + } label: { + VStack(alignment: .leading) { + Text(preset.name) + Text(preset.detail) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + } + } + .navigationTitle(title) + } +} diff --git a/LoopFollowWatch/Remote/RemotePage.swift b/LoopFollowWatch/Remote/RemotePage.swift new file mode 100644 index 000000000..e85b65b6f --- /dev/null +++ b/LoopFollowWatch/Remote/RemotePage.swift @@ -0,0 +1,76 @@ +// LoopFollow +// RemotePage.swift + +import SwiftUI + +/// Remote commands, laid out like the phone's Remote tab. Every command is sent +/// by the phone, which applies its own guardrails before contacting Loop or Trio. +struct RemotePage: View { + let state: WatchState + let remote: WatchRemoteConfig + @EnvironmentObject private var session: WatchSession + + var body: some View { + ScrollView { + VStack(spacing: 8) { + if !session.isReachable { + Label("iPhone not reachable", systemImage: "iphone.slash") + .font(.footnote) + .foregroundStyle(.orange) + } + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) { + NavigationLink { + MealEntryView(remote: remote) + } label: { + RemoteTile(title: "Meal", icon: "fork.knife") + } + NavigationLink { + BolusEntryView(state: state, remote: remote) + } label: { + RemoteTile(title: "Bolus", icon: "syringe") + } + if remote.kind == .trio { + NavigationLink { + PresetListView(kind: .tempTarget, state: state, presets: remote.tempTargetPresets) + } label: { + RemoteTile(title: "Temp Target", icon: "scope", isActive: state.snapshot?.tempTargetMgdl != nil) + } + } + NavigationLink { + PresetListView(kind: .override, state: state, presets: remote.overridePresets) + } label: { + RemoteTile(title: "Overrides", icon: "slider.horizontal.3", isActive: state.snapshot?.override != nil) + } + } + .buttonStyle(.plain) + } + } + .navigationTitle(remote.kind == .trio ? "Trio Remote" : "Loop Remote") + } +} + +/// Same look as the phone's CommandButtonView: blue tile, white icon, green glow while active. +struct RemoteTile: View { + let title: String + let icon: String + var isActive = false + + var body: some View { + VStack(spacing: 4) { + Image(systemName: icon) + .font(.title3) + Text(title) + .font(.footnote) + .lineLimit(1) + .minimumScaleFactor(0.7) + } + .frame(maxWidth: .infinity, minHeight: 64) + .background(Color.blue, in: RoundedRectangle(cornerRadius: 8)) + .foregroundStyle(.white) + .overlay { + if isActive { + RoundedRectangle(cornerRadius: 8).stroke(Color.green, lineWidth: 2) + } + } + } +} diff --git a/LoopFollowWatch/WatchChartView.swift b/LoopFollowWatch/WatchChartView.swift new file mode 100644 index 000000000..164c2e30a --- /dev/null +++ b/LoopFollowWatch/WatchChartView.swift @@ -0,0 +1,153 @@ +// LoopFollow +// WatchChartView.swift + +import Charts +import SwiftUI + +/// BG chart in the phone's colors: readings colored by range, dashed low/high +/// lines, prediction lines or cone in their phone colors, boluses and carbs as markers. +/// Tap to change the time span. +struct WatchChartView: View { + let state: WatchState + let now: Date + + private static let spans: [Double] = [1, 3, 6, 12, 24] + @AppStorage("chartSpanHours") private var spanHours: Double = 3 + + private var unit: GlucoseSnapshot.Unit { state.snapshot?.unit ?? .mgdl } + + var body: some View { + let window = visibleWindow + let readings = state.readings.filter { window.contains($0.date) } + let predictions = state.predictions.map { line in + (line.kind, line.points.filter { window.contains($0.date) }) + } + let cone = state.cone.filter { window.contains($0.date) } + let top = yTop(readings: readings, predictions: predictions.flatMap(\.1) + cone.map { WatchReading(t: $0.t, v: $0.high) }) + + Chart { + RuleMark(y: .value("High", state.highMgdl)) + .foregroundStyle(Color.yellow.opacity(0.5)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 3])) + RuleMark(y: .value("Low", state.lowMgdl)) + .foregroundStyle(Color.red.opacity(0.5)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 3])) + RuleMark(x: .value("Now", now)) + .foregroundStyle(Color.gray.opacity(0.5)) + .lineStyle(StrokeStyle(lineWidth: 1)) + + ForEach(cone, id: \.t) { point in + AreaMark( + x: .value("Time", point.date), + yStart: .value("Low", point.low), + yEnd: .value("High", min(point.high, top)) + ) + .foregroundStyle(Color.blue.opacity(0.4)) + } + + ForEach(predictions, id: \.0) { kind, points in + ForEach(points, id: \.t) { point in + LineMark( + x: .value("Time", point.date), + y: .value("BG", min(point.v, top)), + series: .value("Series", kind.rawValue) + ) + .foregroundStyle(Self.color(for: kind)) + .lineStyle(StrokeStyle(lineWidth: 1.5)) + } + } + + ForEach(readings, id: \.t) { reading in + PointMark( + x: .value("Time", reading.date), + y: .value("BG", min(reading.v, top)) + ) + .symbolSize(spanHours <= 3 ? 14 : 6) + .foregroundStyle(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) + } + + ForEach(state.carbs.filter { window.contains($0.date) }, id: \.t) { carb in + PointMark( + x: .value("Time", carb.date), + y: .value("BG", max(markerY(at: carb.date, top: top) - 30, 45)) + ) + .symbolSize(30) + .foregroundStyle(Color.orange.opacity(0.85)) + } + + ForEach(state.boluses.filter { window.contains($0.date) }, id: \.t) { bolus in + PointMark( + x: .value("Time", bolus.date), + y: .value("BG", markerY(at: bolus.date, top: top) + 30) + ) + .symbol(.triangle) + .symbolSize(24) + .foregroundStyle(Color.blue.opacity(0.85)) + } + } + .chartXScale(domain: window.lowerBound ... window.upperBound) + .chartYScale(domain: 39 ... top) + .chartXAxis { + AxisMarks(values: .stride(by: .hour, count: spanHours > 6 ? 6 : (spanHours > 3 ? 2 : 1))) { _ in + AxisGridLine().foregroundStyle(Color.gray.opacity(0.3)) + AxisValueLabel(format: .dateTime.hour()) + } + } + .chartYAxis { + AxisMarks(position: .trailing, values: yAxisValues(top: top)) { value in + AxisGridLine().foregroundStyle(Color.gray.opacity(0.3)) + AxisValueLabel { + if let mgdl = value.as(Double.self) { + Text(WatchFormat.glucose(mgdl, unit: unit)) + } + } + } + } + .overlay(alignment: .topLeading) { + Text("\(Int(spanHours)) h") + .font(.system(size: 10)) + .foregroundStyle(.secondary) + } + .contentShape(Rectangle()) + .onTapGesture { + let index = Self.spans.firstIndex(of: spanHours) ?? 1 + spanHours = Self.spans[(index + 1) % Self.spans.count] + } + .accessibilityLabel("BG chart, last \(Int(spanHours)) hours. Tap to change the time span.") + } + + /// The visible span ends a little after now so the prediction stays visible. + private var visibleWindow: ClosedRange { + let span = spanHours * 3600 + let lookAhead = state.predictions.isEmpty && state.cone.isEmpty ? span * 0.05 : span * 0.25 + return now.addingTimeInterval(-span + lookAhead) ... now.addingTimeInterval(lookAhead) + } + + private func yTop(readings: [WatchReading], predictions: [WatchReading]) -> Double { + let highest = max(readings.map(\.v).max() ?? 0, predictions.map(\.v).max() ?? 0) + return min(max(250, highest + 20), 400) + } + + private func yAxisValues(top: Double) -> [Double] { + let steps: [Double] = unit == .mmol + ? [72, 144, 216, 288, 360].filter { $0 < top } + : [100, 200, 300].filter { $0 < top } + return steps + } + + /// Markers sit next to the BG reading closest in time: boluses above, carbs below, as on the phone. + private func markerY(at date: Date, top: Double) -> Double { + let nearest = state.readings.min { abs($0.date.timeIntervalSince(date)) < abs($1.date.timeIntervalSince(date)) } + return min(nearest?.v ?? 100, top - 30) + } + + static func color(for kind: WatchPredictionLine.Kind) -> Color { + switch kind { + case .loop: .purple + case .zt: Color(red: 0.443, green: 0.380, blue: 0.937) + case .iob: Color(red: 0.118, green: 0.588, blue: 0.988) + case .cob: .orange + case .uam: Color(red: 0.820, green: 0.169, blue: 0.969) + } + } +} diff --git a/LoopFollowWatch/WatchSession.swift b/LoopFollowWatch/WatchSession.swift new file mode 100644 index 000000000..72e52fb45 --- /dev/null +++ b/LoopFollowWatch/WatchSession.swift @@ -0,0 +1,116 @@ +// LoopFollow +// WatchSession.swift + +import Foundation +import WatchConnectivity +import WatchKit +import WidgetKit + +/// Watch side of the phone link. Receives `WatchState` from the phone and sends +/// commands back for the phone to validate and relay. +@MainActor +final class WatchSession: NSObject, ObservableObject { + static let shared = WatchSession() + + @Published private(set) var state: WatchState? + @Published private(set) var isReachable = false + + private var session: WCSession { WCSession.default } + + override private init() { + state = WatchStateStore.load() + super.init() + } + + func activate() { + guard WCSession.isSupported(), session.delegate == nil else { return } + session.delegate = self + session.activate() + } + + /// Asks the phone for fresh state; the application context covers the case + /// where the phone is out of reach. + func requestState() { + guard session.activationState == .activated, session.isReachable else { return } + session.sendMessage([WatchMessageKey.requestState: true], replyHandler: { reply in + guard let data = reply[WatchState.contextKey] as? Data else { return } + Task { @MainActor in self.apply(data) } + }, errorHandler: nil) + } + + /// Sends a command to the phone and waits for its verdict. The phone has to be + /// reachable: a command is never queued for later delivery. + func send(_ action: WatchCommand.Action) async -> WatchCommandReply { + guard session.activationState == .activated, session.isReachable else { + return WatchCommandReply(success: false, message: "iPhone not reachable. Open LoopFollow on the iPhone and keep it near the watch.") + } + guard let data = try? JSONEncoder().encode(WatchCommand(action: action)) else { + return WatchCommandReply(success: false, message: "Could not encode the command.") + } + return await withCheckedContinuation { continuation in + session.sendMessage([WatchCommand.messageKey: data], replyHandler: { reply in + let result = (reply[WatchCommandReply.messageKey] as? Data) + .flatMap { try? JSONDecoder().decode(WatchCommandReply.self, from: $0) } + continuation.resume(returning: result ?? WatchCommandReply(success: false, message: "Unexpected reply from the iPhone.")) + }, errorHandler: { error in + continuation.resume(returning: WatchCommandReply( + success: false, + message: "No reply from the iPhone (\(error.localizedDescription)). Check LoopFollow on the iPhone before trying again." + )) + }) + } + } + + /// Lets a WatchConnectivity background task finish once queued data has been delivered. + func waitForPendingContent() async { + activate() + for _ in 0 ..< 40 { + if session.activationState == .activated, !session.hasContentPending { return } + try? await Task.sleep(nanoseconds: 500_000_000) + } + } + + private func apply(_ data: Data) { + guard let incoming = WatchState.decode(data) else { return } + if let current = state, current.builtAt > incoming.builtAt { return } + + let newAlarm = incoming.alarm != nil && incoming.alarm?.id != state?.alarm?.id + state = incoming + WatchStateStore.save(data) + WidgetCenter.shared.reloadAllTimelines() + + if newAlarm, WKApplication.shared().applicationState == .active { + WKInterfaceDevice.current().play(.notification) + } + } +} + +extension WatchSession: WCSessionDelegate { + nonisolated func session(_ session: WCSession, activationDidCompleteWith _: WCSessionActivationState, error _: Error?) { + let context = session.receivedApplicationContext[WatchState.contextKey] as? Data + let reachable = session.isReachable + Task { @MainActor in + if let context { self.apply(context) } + self.isReachable = reachable + self.requestState() + } + } + + nonisolated func sessionReachabilityDidChange(_ session: WCSession) { + let reachable = session.isReachable + Task { @MainActor in + self.isReachable = reachable + if reachable { self.requestState() } + } + } + + nonisolated func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { + guard let data = applicationContext[WatchState.contextKey] as? Data else { return } + Task { @MainActor in self.apply(data) } + } + + nonisolated func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { + guard let data = userInfo[WatchState.contextKey] as? Data else { return } + Task { @MainActor in self.apply(data) } + } +} diff --git a/LoopFollowWatchWidgets/GlucoseComplicationView.swift b/LoopFollowWatchWidgets/GlucoseComplicationView.swift new file mode 100644 index 000000000..7e42e5f29 --- /dev/null +++ b/LoopFollowWatchWidgets/GlucoseComplicationView.swift @@ -0,0 +1,132 @@ +// LoopFollow +// GlucoseComplicationView.swift + +import Charts +import SwiftUI +import WidgetKit + +struct GlucoseComplicationView: View { + let entry: GlucoseEntry + @Environment(\.widgetFamily) private var family + + private var snapshot: GlucoseSnapshot? { entry.state?.snapshot } + + private var isStale: Bool { + guard let snapshot else { return true } + return entry.date.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter + } + + private var bgText: String { + guard let snapshot else { return "--" } + return WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit) + } + + private var bgColor: Color { + guard let state = entry.state, let snapshot else { return .secondary } + if isStale { return .secondary } + return WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText) + } + + private var trendText: String { + snapshot.map { WatchFormat.trendArrow($0.trend) } ?? "" + } + + private var deltaText: String { + snapshot.map { WatchFormat.delta($0.delta, unit: $0.unit) } ?? "" + } + + private var ageText: String { + snapshot.map { WatchFormat.minutesAgo($0.updatedAt, now: entry.date) } ?? "" + } + + var body: some View { + switch family { + case .accessoryCircular: + circular + case .accessoryRectangular: + rectangular + case .accessoryCorner: + corner + default: + inline + } + } + + private var circular: some View { + VStack(spacing: -2) { + Text(bgText) + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(bgColor) + .strikethrough(isStale, color: .red) + .minimumScaleFactor(0.5) + Text(trendText) + .font(.system(size: 12, weight: .bold)) + Text(ageText) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + .widgetAccentable() + } + + private var rectangular: some View { + HStack(spacing: 6) { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .firstTextBaseline, spacing: 2) { + Text(bgText) + .font(.system(size: 26, weight: .bold)) + .foregroundStyle(bgColor) + .strikethrough(isStale, color: .red) + Text(trendText) + .font(.system(size: 14, weight: .bold)) + } + Text("\(deltaText) \(ageText)") + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + .lineLimit(1) + .minimumScaleFactor(0.6) + .widgetAccentable() + sparkline + } + } + + /// Last two hours of readings in range colors. + @ViewBuilder + private var sparkline: some View { + if let state = entry.state { + let start = entry.date.addingTimeInterval(-2 * 3600) + let readings = state.readings.filter { $0.date >= start } + Chart { + RuleMark(y: .value("High", state.highMgdl)) + .foregroundStyle(Color.yellow.opacity(0.4)) + .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [2, 2])) + RuleMark(y: .value("Low", state.lowMgdl)) + .foregroundStyle(Color.red.opacity(0.4)) + .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [2, 2])) + ForEach(readings, id: \.t) { reading in + PointMark(x: .value("Time", reading.date), y: .value("BG", min(max(reading.v, 40), 300))) + .symbolSize(6) + .foregroundStyle(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) + } + } + .chartXScale(domain: start ... entry.date) + .chartYScale(domain: 40 ... 300) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } + } + + private var corner: some View { + Text(bgText) + .font(.system(size: 18, weight: .bold)) + .foregroundStyle(bgColor) + .widgetCurvesContent() + .widgetLabel { + Text("\(trendText) \(deltaText) \(ageText)") + } + } + + private var inline: some View { + Text(snapshot == nil ? "LoopFollow --" : "\(bgText) \(trendText) \(deltaText) · \(ageText)") + } +} diff --git a/LoopFollowWatchWidgets/Info.plist b/LoopFollowWatchWidgets/Info.plist new file mode 100644 index 000000000..c8ec08987 --- /dev/null +++ b/LoopFollowWatchWidgets/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(display_name) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(LOOP_FOLLOW_MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/LoopFollowWatchWidgets/LoopFollowWatchWidgets.entitlements b/LoopFollowWatchWidgets/LoopFollowWatchWidgets.entitlements new file mode 100644 index 000000000..5b963cc90 --- /dev/null +++ b/LoopFollowWatchWidgets/LoopFollowWatchWidgets.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.$(unique_id).LoopFollow$(app_suffix) + + + diff --git a/LoopFollowWatchWidgets/LoopFollowWatchWidgets.swift b/LoopFollowWatchWidgets/LoopFollowWatchWidgets.swift new file mode 100644 index 000000000..456d3a9bb --- /dev/null +++ b/LoopFollowWatchWidgets/LoopFollowWatchWidgets.swift @@ -0,0 +1,50 @@ +// LoopFollow +// LoopFollowWatchWidgets.swift + +import SwiftUI +import WidgetKit + +@main +struct LoopFollowWatchWidgets: WidgetBundle { + var body: some Widget { + GlucoseComplication() + } +} + +struct GlucoseComplication: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: "GlucoseComplication", provider: GlucoseTimelineProvider()) { entry in + GlucoseComplicationView(entry: entry) + .containerBackground(.black, for: .widget) + } + .configurationDisplayName("BG") + .description("Latest BG from LoopFollow on your iPhone.") + .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner]) + } +} + +struct GlucoseEntry: TimelineEntry { + let date: Date + let state: WatchState? +} + +/// Reads the state the watch app stored. One entry per minute keeps the +/// "minutes ago" text and the stale marking current until the next reload. +struct GlucoseTimelineProvider: TimelineProvider { + func placeholder(in _: Context) -> GlucoseEntry { + GlucoseEntry(date: Date(), state: nil) + } + + func getSnapshot(in _: Context, completion: @escaping (GlucoseEntry) -> Void) { + completion(GlucoseEntry(date: Date(), state: WatchStateStore.load())) + } + + func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { + let state = WatchStateStore.load() + let now = Date() + let entries = (0 ..< 30).map { minute in + GlucoseEntry(date: now.addingTimeInterval(Double(minute) * 60), state: state) + } + completion(Timeline(entries: entries, policy: .atEnd)) + } +} diff --git a/Shared/AppGroupID.swift b/Shared/AppGroupID.swift index 5eb1187b8..507bfb7c2 100644 --- a/Shared/AppGroupID.swift +++ b/Shared/AppGroupID.swift @@ -58,6 +58,8 @@ enum AppGroupID { ".WidgetExtension", ".Widgets", ".WidgetsExtension", + ".watchkitapp.widgets", + ".watchkitapp", ".Watch", ".WatchExtension", ".CarPlay", diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift new file mode 100644 index 000000000..15ff3ef24 --- /dev/null +++ b/Tests/WatchProtocolTests.swift @@ -0,0 +1,86 @@ +// LoopFollow +// WatchProtocolTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +struct WatchProtocolTests { + private let t0 = Date(timeIntervalSince1970: 1_790_000_000) + + private func state(alarm: WatchAlarm? = nil) -> WatchState { + WatchState( + builtAt: t0, + snapshot: GlucoseSnapshot(glucose: 142, delta: -3, trend: .downSlight, updatedAt: t0, iob: 1.2, cob: 15, projected: 120, unit: .mmol, isNotLooping: false), + readings: [WatchReading(t: t0.timeIntervalSince1970 - 300, v: 145), WatchReading(t: t0.timeIntervalSince1970, v: 142)], + predictions: [WatchPredictionLine(kind: .iob, points: [WatchReading(t: t0.timeIntervalSince1970 + 300, v: 138)])], + cone: [WatchConePoint(t: t0.timeIntervalSince1970 + 300, low: 120, high: 150)], + boluses: [WatchReading(t: t0.timeIntervalSince1970 - 600, v: 2.5)], + carbs: [WatchReading(t: t0.timeIntervalSince1970 - 600, v: 30)], + lowMgdl: 70, + highMgdl: 180, + colorBGText: true, + lastLoopAt: t0, + device: "Trio", + alarm: alarm, + remote: WatchRemoteConfig( + kind: .trio, + maxBolus: 5, + bolusIncrement: 0.05, + maxCarbs: 80, + maxFat: 30, + maxProtein: 30, + mealWithBolus: true, + mealWithFatProtein: false, + recommendedBolus: 0.4, + recommendedBolusAt: t0, + overridePresets: [WatchPreset(id: "Exercise", name: "Exercise", detail: "80 %, 1 h")], + tempTargetPresets: [] + ) + ) + } + + @Test("State survives the round trip through the application context") + func stateRoundTrip() throws { + let original = state(alarm: WatchAlarm(id: UUID(), name: "Low")) + let data = try #require(original.encoded()) + #expect(WatchState.decode(data) == original) + } + + @Test("Commands keep their action, id and creation time") + func commandRoundTrip() throws { + let actions: [WatchCommand.Action] = [ + .bolus(units: 1.25), + .meal(carbs: 30, fat: 10, protein: 5, bolus: 2, consumedAt: t0), + .startOverride(presetID: "Exercise"), + .cancelOverride, + .startTempTarget(presetID: UUID().uuidString), + .cancelTempTarget, + .snoozeAlarm, + ] + for action in actions { + let command = WatchCommand(action: action) + let decoded = try JSONDecoder().decode(WatchCommand.self, from: JSONEncoder().encode(command)) + #expect(decoded == command) + } + } + + @Test("Glucose and delta follow the phone's unit formatting") + func glucoseFormatting() { + #expect(WatchFormat.glucose(142, unit: .mgdl) == "142") + #expect(WatchFormat.delta(-3, unit: .mgdl) == "-3") + #expect(WatchFormat.delta(4, unit: .mgdl) == "+4") + #expect(WatchFormat.delta(0.4, unit: .mgdl) == "0") + let oneMmol = WatchFormat.delta(GlucoseConversion.mmolToMgDl, unit: .mmol) + #expect(oneMmol.hasPrefix("+1") && oneMmol.count == 4) + #expect(!WatchFormat.delta(0.5, unit: .mmol).hasPrefix("+")) + } + + @Test("Range colors match the phone: red at or below low, yellow at or above high") + func rangeColors() { + #expect(WatchFormat.bgColor(70, lowMgdl: 70, highMgdl: 180) == .red) + #expect(WatchFormat.bgColor(71, lowMgdl: 70, highMgdl: 180) == .green) + #expect(WatchFormat.bgColor(180, lowMgdl: 70, highMgdl: 180) == .yellow) + #expect(WatchFormat.bgColor(250, lowMgdl: 70, highMgdl: 180, colored: false) == .primary) + } +} diff --git a/WatchShared/WatchCommand.swift b/WatchShared/WatchCommand.swift new file mode 100644 index 000000000..afda403ba --- /dev/null +++ b/WatchShared/WatchCommand.swift @@ -0,0 +1,45 @@ +// LoopFollow +// WatchCommand.swift + +import Foundation + +/// A request from the watch. The phone validates it against its own settings and +/// guardrails and sends it with the same code path as its Remote screens. +struct WatchCommand: Codable, Equatable { + static let messageKey = "watchCommand" + /// The phone ignores commands older than this, so a delayed delivery is never acted on. + static let maxAge: TimeInterval = 60 + + enum Action: Codable, Equatable { + case bolus(units: Double) + /// Loop sends carbs only; Trio may add fat, protein and a bolus in the same command. + case meal(carbs: Double, fat: Double, protein: Double, bolus: Double, consumedAt: Date) + case startOverride(presetID: String) + case cancelOverride + case startTempTarget(presetID: String) + case cancelTempTarget + case snoozeAlarm + } + + let id: UUID + let createdAt: Date + let action: Action + + init(action: Action) { + id = UUID() + createdAt = Date() + self.action = action + } +} + +struct WatchCommandReply: Codable, Equatable { + static let messageKey = "watchCommandReply" + + let success: Bool + let message: String +} + +enum WatchMessageKey { + /// Watch → phone: asks for the current state; the reply carries `WatchState.contextKey`. + static let requestState = "requestState" +} diff --git a/WatchShared/WatchFormat.swift b/WatchShared/WatchFormat.swift new file mode 100644 index 000000000..24502d318 --- /dev/null +++ b/WatchShared/WatchFormat.swift @@ -0,0 +1,104 @@ +// LoopFollow +// WatchFormat.swift + +import Foundation +import SwiftUI + +/// Display formatting shared by the watch app and its complications. Values +/// arrive in mg/dL and are rendered in the unit chosen on the phone. +enum WatchFormat { + /// A reading this old is shown struck through, as on the phone. + static let staleAfter: TimeInterval = 12 * 60 + + private static let mgdlFormatter: NumberFormatter = { + let nf = NumberFormatter() + nf.numberStyle = .decimal + nf.maximumFractionDigits = 0 + nf.locale = .current + return nf + }() + + private static let oneDecimalFormatter: NumberFormatter = { + let nf = NumberFormatter() + nf.numberStyle = .decimal + nf.minimumFractionDigits = 1 + nf.maximumFractionDigits = 1 + nf.locale = .current + return nf + }() + + private static let insulinFormatter: NumberFormatter = { + let nf = NumberFormatter() + nf.numberStyle = .decimal + nf.minimumFractionDigits = 0 + nf.maximumFractionDigits = 2 + nf.locale = .current + return nf + }() + + static func glucose(_ mgdl: Double, unit: GlucoseSnapshot.Unit) -> String { + switch unit { + case .mgdl: + return mgdlFormatter.string(from: NSNumber(value: round(mgdl))) ?? "\(Int(round(mgdl)))" + case .mmol: + let mmol = GlucoseConversion.toMmol(mgdl) + return oneDecimalFormatter.string(from: NSNumber(value: mmol)) ?? String(format: "%.1f", mmol) + } + } + + static func delta(_ mgdl: Double, unit: GlucoseSnapshot.Unit) -> String { + switch unit { + case .mgdl: + let v = Int(round(mgdl)) + if v == 0 { return "0" } + return v > 0 ? "+\(v)" : "\(v)" + case .mmol: + let mmol = GlucoseConversion.toMmol(mgdl) + let d = abs(mmol) < 0.05 ? 0.0 : mmol + let formatted = oneDecimalFormatter.string(from: NSNumber(value: abs(d))) ?? String(format: "%.1f", abs(d)) + if d == 0 { return formatted } + return d > 0 ? "+\(formatted)" : "-\(formatted)" + } + } + + static func trendArrow(_ trend: GlucoseSnapshot.Trend) -> String { + switch trend { + case .upFast: "↑↑" + case .up: "↑" + case .upSlight: "↗" + case .flat: "→" + case .downSlight: "↘︎" + case .down: "↓" + case .downFast: "↓↓" + case .unknown: "–" + } + } + + static func insulin(_ units: Double) -> String { + insulinFormatter.string(from: NSNumber(value: units)) ?? String(format: "%.2f", units) + } + + static func iob(_ units: Double?) -> String { + guard let units else { return "—" } + return (oneDecimalFormatter.string(from: NSNumber(value: units)) ?? String(format: "%.1f", units)) + " U" + } + + static func cob(_ grams: Double?) -> String { + guard let grams else { return "—" } + return "\(Int(round(grams))) g" + } + + static func minutesAgo(_ date: Date, now: Date = Date()) -> String { + let minutes = max(0, Int(now.timeIntervalSince(date) / 60)) + return "\(minutes) min" + } + + /// Phone semantics: yellow at or above high, red at or below low, green in + /// range. White when the phone's "Color BG Text" setting is off. + static func bgColor(_ mgdl: Double, lowMgdl: Double, highMgdl: Double, colored: Bool = true) -> Color { + guard colored else { return .primary } + if mgdl >= highMgdl { return .yellow } + if mgdl <= lowMgdl { return .red } + return .green + } +} diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift new file mode 100644 index 000000000..f44e1b653 --- /dev/null +++ b/WatchShared/WatchState.swift @@ -0,0 +1,112 @@ +// LoopFollow +// WatchState.swift + +import Foundation + +/// Everything the watch shows, built by the phone and delivered as the +/// WatchConnectivity application context. The watch never talks to Nightscout, +/// Dexcom or APNs itself, so no credentials are part of this state. +struct WatchState: Codable, Equatable { + static let contextKey = "watchState" + + /// When the phone built this state. + var builtAt: Date + /// Latest glucose state, shared with the Live Activity. + var snapshot: GlucoseSnapshot? + /// BG readings for the last 24 hours, oldest first. + var readings: [WatchReading] + /// Prediction lines as drawn on the phone's main chart. + var predictions: [WatchPredictionLine] + /// Trio/OpenAPS forecast drawn as a band when the phone shows the cone. + var cone: [WatchConePoint] + var boluses: [WatchReading] + var carbs: [WatchReading] + /// Range thresholds used for coloring, in mg/dL (same as the phone). + var lowMgdl: Double + var highMgdl: Double + /// Mirrors the phone's "Color BG Text" setting. + var colorBGText: Bool + /// When the loop last reported, nil when unknown. + var lastLoopAt: Date? + /// "Loop", "Trio" or another uploader name; empty for CGM-only setups. + var device: String + /// The alarm currently sounding on the phone. + var alarm: WatchAlarm? + /// Present when remote commands from the watch are enabled on the phone. + var remote: WatchRemoteConfig? + + func encoded() -> Data? { + try? JSONEncoder().encode(self) + } + + static func decode(_ data: Data) -> WatchState? { + try? JSONDecoder().decode(WatchState.self, from: data) + } +} + +struct WatchReading: Codable, Equatable { + /// Unix epoch seconds. + let t: TimeInterval + /// mg/dL for glucose, units for boluses, grams for carbs. + let v: Double + + var date: Date { Date(timeIntervalSince1970: t) } +} + +struct WatchPredictionLine: Codable, Equatable { + /// Matches the phone's chart series and colors. + enum Kind: String, Codable { + case loop + case zt + case iob + case cob + case uam + } + + let kind: Kind + let points: [WatchReading] +} + +struct WatchConePoint: Codable, Equatable { + let t: TimeInterval + let low: Double + let high: Double + + var date: Date { Date(timeIntervalSince1970: t) } +} + +struct WatchAlarm: Codable, Equatable { + let id: UUID + let name: String +} + +struct WatchPreset: Codable, Equatable, Identifiable { + let id: String + let name: String + /// Preformatted on the phone in the user's units, e.g. "120 %, 1 h". + let detail: String +} + +struct WatchRemoteConfig: Codable, Equatable { + enum Kind: String, Codable { + case loop + case trio + } + + let kind: Kind + let maxBolus: Double + let bolusIncrement: Double + let maxCarbs: Double + let maxFat: Double + let maxProtein: Double + /// Trio only: a meal may carry a bolus. + let mealWithBolus: Bool + /// Trio only: a meal may carry fat and protein. + let mealWithFatProtein: Bool + /// The AID's own recommendation, as shown in the phone's bolus screen. + let recommendedBolus: Double? + let recommendedBolusAt: Date? + let overridePresets: [WatchPreset] + /// Trio only. + let tempTargetPresets: [WatchPreset] +} diff --git a/WatchShared/WatchStateStore.swift b/WatchShared/WatchStateStore.swift new file mode 100644 index 000000000..65c078618 --- /dev/null +++ b/WatchShared/WatchStateStore.swift @@ -0,0 +1,26 @@ +// LoopFollow +// WatchStateStore.swift + +import Foundation + +/// Keeps the last state received from the phone in the App Group container, so +/// the watch app shows data at launch and the complications can read it. +enum WatchStateStore { + private static let fileName = "watch-state.json" + + private static var fileURL: URL? { + FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: AppGroupID.current())? + .appendingPathComponent(fileName) + } + + static func save(_ data: Data) { + guard let url = fileURL else { return } + try? data.write(to: url, options: .atomic) + } + + static func load() -> WatchState? { + guard let url = fileURL, let data = try? Data(contentsOf: url) else { return nil } + return WatchState.decode(data) + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8f46645c5..88a852749 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -43,6 +43,8 @@ end APP_SUFFIX = loopfollow_app_suffix APP_IDENTIFIER = "com.#{TEAMID}.LoopFollow#{APP_SUFFIX}" LA_IDENTIFIER = "#{APP_IDENTIFIER}.LoopFollowLAExtension" +WATCH_IDENTIFIER = "#{APP_IDENTIFIER}.watchkitapp" +WATCH_WIDGETS_IDENTIFIER = "#{WATCH_IDENTIFIER}.widgets" platform :ios do desc "Build Loop Follow" @@ -77,7 +79,9 @@ platform :ios do git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), app_identifier: [ APP_IDENTIFIER, - LA_IDENTIFIER + LA_IDENTIFIER, + WATCH_IDENTIFIER, + WATCH_WIDGETS_IDENTIFIER ] ) @@ -99,6 +103,20 @@ platform :ios do targets: ["LoopFollowLAExtensionExtension"] ) + update_code_signing_settings( + path: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj", + profile_name: mapping[WATCH_IDENTIFIER], + code_sign_identity: "iPhone Distribution", + targets: ["LoopFollowWatch"] + ) + + update_code_signing_settings( + path: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj", + profile_name: mapping[WATCH_WIDGETS_IDENTIFIER], + code_sign_identity: "iPhone Distribution", + targets: ["LoopFollowWatchWidgets"] + ) + gym( export_method: "app-store", scheme: "LoopFollow", @@ -109,7 +127,9 @@ platform :ios do export_options: { provisioningProfiles: { APP_IDENTIFIER => mapping[APP_IDENTIFIER], - LA_IDENTIFIER => mapping[LA_IDENTIFIER] + LA_IDENTIFIER => mapping[LA_IDENTIFIER], + WATCH_IDENTIFIER => mapping[WATCH_IDENTIFIER], + WATCH_WIDGETS_IDENTIFIER => mapping[WATCH_WIDGETS_IDENTIFIER] } } ) @@ -168,6 +188,14 @@ platform :ios do Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS ]) + configure_bundle_id("LoopFollow Watch App", WATCH_IDENTIFIER, [ + Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS + ]) + + configure_bundle_id("LoopFollow Watch Complications", WATCH_WIDGETS_IDENTIFIER, [ + Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS + ]) + end desc "Provision Certificates" @@ -188,7 +216,9 @@ platform :ios do git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), app_identifier: [ APP_IDENTIFIER, - LA_IDENTIFIER + LA_IDENTIFIER, + WATCH_IDENTIFIER, + WATCH_WIDGETS_IDENTIFIER ] ) end From f761eec537fb49c78c4d460b8e26b57ced8162ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 12:17:19 +0200 Subject: [PATCH 02/25] Put the watch Remote page before Info, allow watch commands by default, and limit complication reloads to new readings Remote tiles are disabled while the iPhone is out of reach. --- LoopFollow/Storage/Storage.swift | 2 +- LoopFollowWatch/ContentView.swift | 3 ++- LoopFollowWatch/Remote/RemotePage.swift | 2 ++ LoopFollowWatch/WatchSession.swift | 11 ++++++++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 1cd80fb64..ddb47e7b5 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -35,7 +35,7 @@ class Storage { var mealWithBolus = StorageValue(key: "mealWithBolus", defaultValue: false) var mealWithFatProtein = StorageValue(key: "mealWithFatProtein", defaultValue: false) /// Lets the Apple Watch app send remote commands through this phone. - var watchRemoteCommands = StorageValue(key: "watchRemoteCommands", defaultValue: false) + var watchRemoteCommands = StorageValue(key: "watchRemoteCommands", defaultValue: true) // TODO: This flag can be deleted in March 2027. Check the commit for other places to cleanup. var hasSeenFatProteinOrderChange = StorageValue(key: "hasSeenFatProteinOrderChange", defaultValue: true) diff --git a/LoopFollowWatch/ContentView.swift b/LoopFollowWatch/ContentView.swift index 52069e8f9..f27af7664 100644 --- a/LoopFollowWatch/ContentView.swift +++ b/LoopFollowWatch/ContentView.swift @@ -11,10 +11,11 @@ struct ContentView: View { if let state = session.state { TabView { GlucosePage(state: state) - InfoPage(state: state) if let remote = state.remote { RemotePage(state: state, remote: remote) } + // Last, so its length never stands between the other pages. + InfoPage(state: state) } .tabViewStyle(.verticalPage) } else { diff --git a/LoopFollowWatch/Remote/RemotePage.swift b/LoopFollowWatch/Remote/RemotePage.swift index e85b65b6f..c21b96470 100644 --- a/LoopFollowWatch/Remote/RemotePage.swift +++ b/LoopFollowWatch/Remote/RemotePage.swift @@ -43,6 +43,8 @@ struct RemotePage: View { } } .buttonStyle(.plain) + .disabled(!session.isReachable) + .opacity(session.isReachable ? 1 : 0.4) } } .navigationTitle(remote.kind == .trio ? "Trio Remote" : "Loop Remote") diff --git a/LoopFollowWatch/WatchSession.swift b/LoopFollowWatch/WatchSession.swift index 72e52fb45..6b7f81b5d 100644 --- a/LoopFollowWatch/WatchSession.swift +++ b/LoopFollowWatch/WatchSession.swift @@ -70,14 +70,23 @@ final class WatchSession: NSObject, ObservableObject { } } + private static func complicationKey(_ state: WatchState) -> String { + let s = state.snapshot + return "\(s?.updatedAt.timeIntervalSince1970 ?? 0)|\(s?.unit.rawValue ?? "")|\(state.lowMgdl)|\(state.highMgdl)|\(state.colorBGText)" + } + private func apply(_ data: Data) { guard let incoming = WatchState.decode(data) else { return } if let current = state, current.builtAt > incoming.builtAt { return } let newAlarm = incoming.alarm != nil && incoming.alarm?.id != state?.alarm?.id + let complicationChanged = Self.complicationKey(incoming) != state.map(Self.complicationKey) state = incoming WatchStateStore.save(data) - WidgetCenter.shared.reloadAllTimelines() + // Widget reloads are budgeted, so only reload when something the complication shows has changed. + if complicationChanged { + WidgetCenter.shared.reloadAllTimelines() + } if newAlarm, WKApplication.shared().applicationState == .active { WKInterfaceDevice.current().play(.notification) From 56fa351df23e401e3cd3c6c8fd1de1647d4c7c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 12:59:40 +0200 Subject: [PATCH 03/25] Make the watch app glanceable: one large BG, actions in the bottom toolbar, quick picks The glucose page shows BG with trend and delta, IOB and age in the top bar, a two-hour sparkline, and Override, Meal or Bolus, and Temp Target in the bottom toolbar. The full chart has its own page. Bolus offers the AID's recommendation and the phone's Quick-Pick boluses before a custom amount; Meal shows Quick-Pick meals and carbs, with fat, protein, bolus and time under More. A successful send shows Sent, notes that Loop or Trio confirms with a notification, and returns to the glucose page. --- LoopFollow/Watch/WatchStateBuilder.swift | 29 ++- LoopFollowWatch/ChartPage.swift | 17 ++ LoopFollowWatch/ContentView.swift | 69 ++++++- LoopFollowWatch/GlucosePage.swift | 190 ++++++++++++++---- LoopFollowWatch/Remote/BolusEntryView.swift | 121 ++++++----- .../Remote/CommandConfirmView.swift | 55 +++-- LoopFollowWatch/Remote/MealEntryView.swift | 105 +++++++--- LoopFollowWatch/Remote/PresetListView.swift | 20 +- LoopFollowWatch/Remote/RemotePage.swift | 78 ------- .../Remote/TreatmentMenuView.swift | 24 +++ Tests/WatchProtocolTests.swift | 4 +- WatchShared/WatchCommand.swift | 2 +- WatchShared/WatchState.swift | 10 + 13 files changed, 490 insertions(+), 234 deletions(-) create mode 100644 LoopFollowWatch/ChartPage.swift delete mode 100644 LoopFollowWatch/Remote/RemotePage.swift create mode 100644 LoopFollowWatch/Remote/TreatmentMenuView.swift diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index 359602522..baac97fa7 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -88,23 +88,42 @@ enum WatchStateBuilder { private static func remoteConfig() -> WatchRemoteConfig? { guard Storage.shared.watchRemoteCommands.value, let kind = remoteKind() else { return nil } + let maxBolus = Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()) + let bolusIncrement = max(0.001, Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit())) + let maxCarbs = Storage.shared.maxCarbs.value.doubleValue(for: .gram()) + let mealWithFatProtein = kind == .trio && Storage.shared.mealWithFatProtein.value + let now = Date() + let recommendedAt: TimeInterval? = kind == .trio ? Observable.shared.enactedOrSuggested.value : Observable.shared.alertLastLoopTime.value return WatchRemoteConfig( kind: kind, - maxBolus: Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()), - bolusIncrement: max(0.001, Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit())), - maxCarbs: Storage.shared.maxCarbs.value.doubleValue(for: .gram()), + maxBolus: maxBolus, + bolusIncrement: bolusIncrement, + maxCarbs: maxCarbs, maxFat: Storage.shared.maxFat.value.doubleValue(for: .gram()), maxProtein: Storage.shared.maxProtein.value.doubleValue(for: .gram()), mealWithBolus: kind == .trio && Storage.shared.mealWithBolus.value, - mealWithFatProtein: kind == .trio && Storage.shared.mealWithFatProtein.value, + mealWithFatProtein: mealWithFatProtein, recommendedBolus: Observable.shared.deviceRecBolus.value, recommendedBolusAt: recommendedAt.map { Date(timeIntervalSince1970: $0) }, overridePresets: kind == .trio ? trioOverridePresets() : loopOverridePresets(), - tempTargetPresets: kind == .trio ? tempTargetPresets() : [] + tempTargetPresets: kind == .trio ? tempTargetPresets() : [], + quickPickBoluses: QuickPickBolusesManager.computeQuickPickBoluses( + from: Storage.shared.remoteBolusHistory.value, + now: now, + stepIncrement: bolusIncrement, + maxBolus: maxBolus + ).map(\.units), + quickPickMeals: QuickPickMealsManager.computeQuickPickMeals( + from: Storage.shared.remoteMealHistory.value, + now: now, + carbStep: 1, + maxCarbs: maxCarbs, + includeFatProtein: mealWithFatProtein + ).map { WatchMealPick(carbs: $0.carbs, fat: $0.fat, protein: $0.protein, bolus: $0.bolus) } ) } diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift new file mode 100644 index 000000000..59519adc9 --- /dev/null +++ b/LoopFollowWatch/ChartPage.swift @@ -0,0 +1,17 @@ +// LoopFollow +// ChartPage.swift + +import SwiftUI + +/// The full chart on its own page. +struct ChartPage: View { + let state: WatchState + + var body: some View { + TimelineView(.periodic(from: .now, by: 60)) { context in + WatchChartView(state: state, now: context.date) + // Keeps the axis labels clear of the page indicator. + .padding(.trailing, 10) + } + } +} diff --git a/LoopFollowWatch/ContentView.swift b/LoopFollowWatch/ContentView.swift index f27af7664..0601f7c02 100644 --- a/LoopFollowWatch/ContentView.swift +++ b/LoopFollowWatch/ContentView.swift @@ -3,25 +3,84 @@ import SwiftUI +/// Screens pushed on the main navigation stack. +enum WatchRoute: Hashable { + case treatments + case meal + case bolus + case bolusAmount + case overrides + case tempTargets + case confirm(CommandRequest) +} + +/// What a confirmation screen sends, and how it describes it. +struct CommandRequest: Hashable { + let title: String + let lines: [String] + let action: WatchCommand.Action + let deliversInsulin: Bool +} + +/// Owns the navigation path so a finished command can return to the glucose page. +final class WatchNavigator: ObservableObject { + @Published var path: [WatchRoute] = [] + + func push(_ route: WatchRoute) { + path.append(route) + } + + func popToRoot() { + path.removeAll() + } +} + struct ContentView: View { @EnvironmentObject private var session: WatchSession + @StateObject private var navigator = WatchNavigator() var body: some View { - NavigationStack { + NavigationStack(path: $navigator.path) { if let state = session.state { TabView { GlucosePage(state: state) - if let remote = state.remote { - RemotePage(state: state, remote: remote) - } - // Last, so its length never stands between the other pages. + ChartPage(state: state) InfoPage(state: state) } .tabViewStyle(.verticalPage) + .navigationDestination(for: WatchRoute.self) { route in + destination(route, state: state) + } } else { WaitingForPhoneView() } } + .environmentObject(navigator) + } + + @ViewBuilder + private func destination(_ route: WatchRoute, state: WatchState) -> some View { + if let remote = state.remote { + switch route { + case .treatments: + TreatmentMenuView() + case .meal: + MealEntryView(remote: remote) + case .bolus: + BolusPickerView(remote: remote) + case .bolusAmount: + BolusAmountView(remote: remote) + case .overrides: + PresetListView(kind: .override, state: state, presets: remote.overridePresets) + case .tempTargets: + PresetListView(kind: .tempTarget, state: state, presets: remote.tempTargetPresets) + case let .confirm(request): + CommandConfirmView(request: request) + } + } else { + Text("Remote commands are turned off in LoopFollow on the iPhone.") + .multilineTextAlignment(.center) + } } } diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index 4f4c18d60..0d4f302a0 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -1,77 +1,191 @@ // LoopFollow // GlucosePage.swift +import Charts import SwiftUI -/// Main page: BG, trend and delta as on the phone's home screen, with the chart below. +/// Main page: one large BG value with trend and delta, a two-hour sparkline, +/// and the remote commands in the bottom toolbar. struct GlucosePage: View { let state: WatchState + @EnvironmentObject private var session: WatchSession + @Environment(\.isLuminanceReduced) private var isLuminanceReduced + var body: some View { TimelineView(.periodic(from: .now, by: 30)) { context in VStack(spacing: 2) { if let alarm = state.alarm { AlarmBanner(alarm: alarm) } - header(now: context.date) - WatchChartView(state: state, now: context.date) - footer + glucose(now: context.date) + if !isLuminanceReduced { + details + Sparkline(state: state, now: context.date) + .frame(minHeight: 36) + } } - } - .navigationTitle { - if let snapshot = state.snapshot { - Text(WatchFormat.minutesAgo(snapshot.updatedAt)) - .foregroundStyle(snapshot.age >= WatchFormat.staleAfter ? .red : .secondary) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if !isLuminanceReduced, let iob = state.snapshot?.iob { + Text("IOB \(WatchFormat.iob(iob))") + .font(.footnote) + .lineLimit(1) + .minimumScaleFactor(0.6) + } + } + ToolbarItem(placement: .topBarTrailing) { + age(now: context.date) + } + if !isLuminanceReduced, let remote = state.remote { + ToolbarItemGroup(placement: .bottomBar) { + RemoteToolbar(state: state, remote: remote) + } + } } } - .navigationBarTitleDisplayMode(.inline) } @ViewBuilder - private func header(now: Date) -> some View { + private func glucose(now: Date) -> some View { if let snapshot = state.snapshot { let isStale = now.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter - HStack(alignment: .center, spacing: 6) { - Text(WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit)) - .font(.system(size: 40, weight: .black)) - .foregroundStyle(WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText)) - .strikethrough(isStale, pattern: .solid, color: .red) - .lineLimit(1) - .minimumScaleFactor(0.5) - VStack(alignment: .leading, spacing: 0) { + VStack(spacing: -4) { + HStack(alignment: .center, spacing: 4) { + Text(WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit)) + .font(.system(size: 56, weight: .black)) + .foregroundStyle(WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText)) + .strikethrough(isStale, pattern: .solid, color: .red) + .lineLimit(1) + .minimumScaleFactor(0.5) Text(WatchFormat.trendArrow(snapshot.trend)) - .font(.system(size: 22, weight: .black)) - Text(WatchFormat.delta(snapshot.delta, unit: snapshot.unit)) - .font(.system(size: 15)) - .foregroundStyle(.secondary) + .font(.system(size: 28, weight: .black)) } - .lineLimit(1) - Spacer(minLength: 0) + Text(WatchFormat.delta(snapshot.delta, unit: snapshot.unit)) + .font(.title3) + .foregroundStyle(.secondary) } + .frame(maxWidth: .infinity) + .layoutPriority(1) + .accessibilityElement(children: .combine) + } else { + Text("No BG data") + .font(.headline) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private func age(now: Date) -> some View { + if !session.isReachable { + Image(systemName: "iphone.slash") + .foregroundStyle(.orange) + .accessibilityLabel("iPhone not reachable") + } else if let snapshot = state.snapshot { + Text(WatchFormat.minutesAgo(snapshot.updatedAt, now: now)) + .font(.footnote) + .foregroundStyle(now.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter ? .red : .secondary) + } + } + + @ViewBuilder + private var details: some View { + if let snapshot = state.snapshot { if snapshot.isNotLooping { Label("Not looping", systemImage: "exclamationmark.triangle.fill") .font(.footnote) .foregroundStyle(.red) + } else if snapshot.cob != nil || state.lastLoopAt != nil { + HStack { + if let cob = snapshot.cob { + Text("COB \(WatchFormat.cob(cob))") + } + Spacer(minLength: 4) + if let lastLoop = state.lastLoopAt { + Text("Loop \(WatchFormat.minutesAgo(lastLoop))") + } + } + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.7) } + } + } +} + +/// Override, treatments and temp target, as in Apple's bottom-toolbar pattern. +private struct RemoteToolbar: View { + let state: WatchState + let remote: WatchRemoteConfig + @EnvironmentObject private var session: WatchSession + @EnvironmentObject private var navigator: WatchNavigator + + var body: some View { + Button { + navigator.push(.overrides) + } label: { + Image(systemName: "slider.horizontal.3") + } + .tint(state.snapshot?.override != nil ? .green : nil) + .accessibilityLabel("Overrides") + .disabled(!session.isReachable) + + Button { + navigator.push(.treatments) + } label: { + Image(systemName: "plus") + } + .controlSize(.large) + .buttonStyle(.borderedProminent) + .tint(.blue) + .accessibilityLabel("Meal or bolus") + .disabled(!session.isReachable) + + if remote.kind == .trio { + Button { + navigator.push(.tempTargets) + } label: { + Image(systemName: "scope") + } + .tint(state.snapshot?.tempTargetMgdl != nil ? .green : nil) + .accessibilityLabel("Temp Target") + .disabled(!session.isReachable) } else { - Text("No BG data") - .font(.headline) - .foregroundStyle(.secondary) + Spacer() } } +} - @ViewBuilder - private var footer: some View { - if let snapshot = state.snapshot, snapshot.iob != nil || snapshot.cob != nil { - HStack { - Text("IOB \(WatchFormat.iob(snapshot.iob))") - Spacer() - Text("COB \(WatchFormat.cob(snapshot.cob))") +/// The last two hours in range colors, without axes, scaled to the readings and the range lines. +struct Sparkline: View { + let state: WatchState + let now: Date + + var body: some View { + let start = now.addingTimeInterval(-2 * 3600) + let readings = state.readings.filter { $0.date >= start } + let values = readings.map(\.v) + let bottom = min(values.min() ?? state.lowMgdl, state.lowMgdl) - 10 + let top = max(values.max() ?? state.highMgdl, state.highMgdl) + 10 + Chart { + RuleMark(y: .value("High", state.highMgdl)) + .foregroundStyle(Color.yellow.opacity(0.5)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3])) + RuleMark(y: .value("Low", state.lowMgdl)) + .foregroundStyle(Color.red.opacity(0.5)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3])) + ForEach(readings, id: \.t) { reading in + PointMark(x: .value("Time", reading.date), y: .value("BG", reading.v)) + .symbolSize(12) + .foregroundStyle(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) } - .font(.footnote) - .lineLimit(1) - .minimumScaleFactor(0.7) } + .chartXScale(domain: start ... now) + .chartYScale(domain: bottom ... top) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + .accessibilityHidden(true) } } diff --git a/LoopFollowWatch/Remote/BolusEntryView.swift b/LoopFollowWatch/Remote/BolusEntryView.swift index 157fd2276..39a218c55 100644 --- a/LoopFollowWatch/Remote/BolusEntryView.swift +++ b/LoopFollowWatch/Remote/BolusEntryView.swift @@ -3,56 +3,43 @@ import SwiftUI -struct BolusEntryView: View { - let state: WatchState +/// Pick an amount: the AID's recommendation, the phone's Quick-Pick boluses, or +/// any amount with the Digital Crown. Every choice still needs the crown confirmation. +struct BolusPickerView: View { let remote: WatchRemoteConfig - @Environment(\.dismiss) private var dismiss - @State private var units = 0.0 + @EnvironmentObject private var navigator: WatchNavigator @State private var staleRecommendation: Double? - @FocusState private var amountFocused: Bool /// Same cut-offs as the phone's bolus screens. private let recommendationMaxAge: TimeInterval = 12 * 60 private let recommendationWarnAge: TimeInterval = 5 * 60 - private var step: Double { max(remote.bolusIncrement, 0.025) } + private var step: Double { BolusAmountView.step(remote) } var body: some View { - ScrollView { - VStack(spacing: 8) { - AmountField(value: $units, unit: "U", step: step, maximum: remote.maxBolus, isHighlighted: amountFocused, format: WatchFormat.insulin) - .focused($amountFocused) - Text("Max \(WatchFormat.insulin(remote.maxBolus)) U") - .font(.footnote) - .foregroundStyle(.secondary) - - recommendation - - NavigationLink { - CommandConfirmView( - title: "Bolus", - lines: ["\(WatchFormat.insulin(units)) U"], - action: .bolus(units: units), - deliversInsulin: true, - onSuccess: { dismiss() } - ) + List { + recommendation + ForEach(remote.quickPickBoluses.filter { $0 > 0 && $0 <= remote.maxBolus }, id: \.self) { units in + Button { + confirm(units) } label: { - Text("Continue") + Text("\(WatchFormat.insulin(units)) U") } - .buttonStyle(.borderedProminent) - .tint(.blue) - .disabled(units <= 0) + } + Button { + navigator.push(.bolusAmount) + } label: { + Label("Other Amount", systemImage: "digitalcrown.horizontal.arrow.clockwise") } } .navigationTitle("Bolus") - .onAppear { amountFocused = true } .alert( "Old Calculation Warning", isPresented: Binding(get: { staleRecommendation != nil }, set: { if !$0 { staleRecommendation = nil } }) ) { Button("Use Anyway") { - if let staleRecommendation { units = staleRecommendation } + if let staleRecommendation { confirm(staleRecommendation) } } Button("Cancel", role: .cancel) {} } message: { @@ -65,27 +52,67 @@ struct BolusEntryView: View { if let rec = remote.recommendedBolus, rec > 0, let at = remote.recommendedBolusAt { let age = Date().timeIntervalSince(at) if age < recommendationMaxAge { - let stepped = Self.roundedDown(min(rec, remote.maxBolus), step: step) - Button { - if age >= recommendationWarnAge { - staleRecommendation = stepped - } else { - units = stepped - } - } label: { - VStack(alignment: .leading, spacing: 2) { - Text("Recommended \(WatchFormat.insulin(stepped)) U") - Text("Calculated \(Int(age / 60)) min ago") - .font(.caption2) - .foregroundStyle(.secondary) + let stepped = BolusAmountView.roundedDown(min(rec, remote.maxBolus), step: step) + Section { + Button { + if age >= recommendationWarnAge { + staleRecommendation = stepped + } else { + confirm(stepped) + } + } label: { + VStack(alignment: .leading, spacing: 2) { + Text("Recommended \(WatchFormat.insulin(stepped)) U") + Text("Calculated \(Int(age / 60)) min ago") + .font(.caption2) + .foregroundStyle(age >= recommendationWarnAge ? .red : .secondary) + } } - .frame(maxWidth: .infinity, alignment: .leading) + .disabled(stepped <= 0) + } + } + } + } + + private func confirm(_ units: Double) { + navigator.push(.confirm(BolusAmountView.request(units))) + } +} + +/// Any amount, set with the Digital Crown. +struct BolusAmountView: View { + let remote: WatchRemoteConfig + + @EnvironmentObject private var navigator: WatchNavigator + @State private var units = 0.0 + @FocusState private var amountFocused: Bool + + var body: some View { + ScrollView { + VStack(spacing: 8) { + AmountField(value: $units, unit: "U", step: Self.step(remote), maximum: remote.maxBolus, isHighlighted: amountFocused, format: WatchFormat.insulin) + .focused($amountFocused) + Text("Max \(WatchFormat.insulin(remote.maxBolus)) U") + .font(.footnote) + .foregroundStyle(.secondary) + Button("Continue") { + navigator.push(.confirm(Self.request(units))) } - Text("New treatments may have occurred since the recommended bolus was calculated.") - .font(.caption2) - .foregroundStyle(age >= recommendationWarnAge ? .red : .yellow) + .buttonStyle(.borderedProminent) + .tint(.blue) + .disabled(units <= 0) } } + .navigationTitle("Bolus") + .onAppear { amountFocused = true } + } + + static func step(_ remote: WatchRemoteConfig) -> Double { + max(remote.bolusIncrement, 0.025) + } + + static func request(_ units: Double) -> CommandRequest { + CommandRequest(title: "Bolus", lines: ["\(WatchFormat.insulin(units)) U"], action: .bolus(units: units), deliversInsulin: true) } static func roundedDown(_ value: Double, step: Double) -> Double { diff --git a/LoopFollowWatch/Remote/CommandConfirmView.swift b/LoopFollowWatch/Remote/CommandConfirmView.swift index da224b1ea..dcfb58ec2 100644 --- a/LoopFollowWatch/Remote/CommandConfirmView.swift +++ b/LoopFollowWatch/Remote/CommandConfirmView.swift @@ -7,18 +7,18 @@ import WatchKit /// Final step for every command: shows what will be sent, asks for confirmation /// and shows the phone's reply. Insulin needs a full turn of the Digital Crown -/// and an unlocked watch on the wrist. +/// and an unlocked watch on the wrist. A sent command is not yet delivered: +/// Loop or Trio confirms it later with a notification. struct CommandConfirmView: View { - let title: String - let lines: [String] - let action: WatchCommand.Action - let deliversInsulin: Bool - /// Called when the user closes the result of a successful send. - var onSuccess: () -> Void = {} + let request: CommandRequest @EnvironmentObject private var session: WatchSession + @EnvironmentObject private var navigator: WatchNavigator @Environment(\.dismiss) private var dismiss + /// How long the "sent" screen stays before returning to the glucose page. + private let sentDisplayTime: TimeInterval = 3 + @State private var phase: Phase = .confirm @State private var crownProgress = 0.0 @State private var lastCrownChange = Date() @@ -36,7 +36,7 @@ struct CommandConfirmView: View { switch phase { case .confirm: summary - if deliversInsulin { + if request.deliversInsulin { crownConfirm } else { Button("Send") { Task { await send() } } @@ -48,27 +48,36 @@ struct CommandConfirmView: View { Text("Sending…") .foregroundStyle(.secondary) case let .done(success, message): - Image(systemName: success ? "checkmark.circle.fill" : "xmark.octagon.fill") - .font(.largeTitle) - .foregroundStyle(success ? .green : .red) - Text(message) - .font(.footnote) - .multilineTextAlignment(.center) - Button("Done") { - dismiss() - if success { onSuccess() } + if success { + Image(systemName: "paperplane.fill") + .font(.largeTitle) + .foregroundStyle(.blue) + Text("Sent") + .font(.headline) + Text(message) + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } else { + Image(systemName: "xmark.octagon.fill") + .font(.largeTitle) + .foregroundStyle(.red) + Text(message) + .font(.footnote) + .multilineTextAlignment(.center) + Button("OK") { dismiss() } } } } .frame(maxWidth: .infinity) } - .navigationTitle(title) - .navigationBarBackButtonHidden(phase == .sending) + .navigationTitle(request.title) + .navigationBarBackButtonHidden(phase != .confirm) } private var summary: some View { VStack(spacing: 2) { - ForEach(lines, id: \.self) { line in + ForEach(request.lines, id: \.self) { line in Text(line) .font(.headline) .multilineTextAlignment(.center) @@ -132,9 +141,13 @@ struct CommandConfirmView: View { private func send() async { phase = .sending - let reply = await session.send(action) + let reply = await session.send(request.action) phase = .done(success: reply.success, message: reply.message) WKInterfaceDevice.current().play(reply.success ? .success : .failure) + if reply.success { + try? await Task.sleep(nanoseconds: UInt64(sentDisplayTime * 1_000_000_000)) + navigator.popToRoot() + } } private enum AuthOutcome { diff --git a/LoopFollowWatch/Remote/MealEntryView.swift b/LoopFollowWatch/Remote/MealEntryView.swift index 6b78681fa..c64175277 100644 --- a/LoopFollowWatch/Remote/MealEntryView.swift +++ b/LoopFollowWatch/Remote/MealEntryView.swift @@ -3,58 +3,65 @@ import SwiftUI -/// Carbs for Loop; carbs, fat, protein and an optional bolus for Trio, following -/// the phone's Meal settings. Everything goes to the phone as one command. +/// Carbs first; fat, protein, bolus (Trio, per the phone's Meal settings) and +/// time behind "More". Everything goes to the phone as one command. struct MealEntryView: View { let remote: WatchRemoteConfig - @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var navigator: WatchNavigator @State private var carbs = 0.0 @State private var fat = 0.0 @State private var protein = 0.0 @State private var bolus = 0.0 @State private var offsetMinutes = 0.0 + @State private var showMore = false @FocusState private var focus: Field? private enum Field: Hashable { case carbs, fat, protein, bolus, time } - private var bolusStep: Double { max(remote.bolusIncrement, 0.025) } + private var bolusStep: Double { BolusAmountView.step(remote) } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 6) { + quickPicks + field("Carbs", .carbs) { AmountField(value: $carbs, unit: "g", step: 1, maximum: remote.maxCarbs, isHighlighted: focus == .carbs, format: grams) } - if remote.mealWithFatProtein { - field("Fat", .fat) { - AmountField(value: $fat, unit: "g", step: 1, maximum: remote.maxFat, isHighlighted: focus == .fat, format: grams) + + if showMore { + if remote.mealWithFatProtein { + field("Fat", .fat) { + AmountField(value: $fat, unit: "g", step: 1, maximum: remote.maxFat, isHighlighted: focus == .fat, format: grams) + } + field("Protein", .protein) { + AmountField(value: $protein, unit: "g", step: 1, maximum: remote.maxProtein, isHighlighted: focus == .protein, format: grams) + } } - field("Protein", .protein) { - AmountField(value: $protein, unit: "g", step: 1, maximum: remote.maxProtein, isHighlighted: focus == .protein, format: grams) + if remote.mealWithBolus { + field("Bolus", .bolus) { + AmountField(value: $bolus, unit: "U", step: bolusStep, maximum: remote.maxBolus, isHighlighted: focus == .bolus, format: WatchFormat.insulin) + } } - } - if remote.mealWithBolus { - field("Bolus", .bolus) { - AmountField(value: $bolus, unit: "U", step: bolusStep, maximum: remote.maxBolus, isHighlighted: focus == .bolus, format: WatchFormat.insulin) + field("Time", .time) { + AmountField(value: $offsetMinutes, unit: "min", step: 5, minimum: -120, maximum: 60, isHighlighted: focus == .time, format: timeText) + } + } else { + Button { + showMore = true + } label: { + Label(moreTitle, systemImage: "ellipsis.circle") + .font(.footnote) } - } - field("Time", .time) { - AmountField(value: $offsetMinutes, unit: "min", step: 5, minimum: -120, maximum: 60, isHighlighted: focus == .time, format: timeText) } - NavigationLink { - CommandConfirmView( - title: "Meal", - lines: summaryLines, - action: .meal(carbs: carbs, fat: fat, protein: protein, bolus: bolus, consumedAt: Date().addingTimeInterval(offsetMinutes * 60)), - deliversInsulin: bolus > 0, - onSuccess: { dismiss() } - ) + Button { + navigator.push(.confirm(request(carbs: carbs, fat: fat, protein: protein, bolus: bolus, offsetMinutes: offsetMinutes))) } label: { - Text("Continue") + Text(logTitle) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) @@ -66,6 +73,25 @@ struct MealEntryView: View { .onAppear { focus = .carbs } } + /// The phone's Quick-Pick meals, limited to what the phone's Meal settings allow. + @ViewBuilder + private var quickPicks: some View { + let picks = remote.quickPickMeals.filter { $0.carbs > 0 && $0.carbs <= remote.maxCarbs } + if !picks.isEmpty { + ForEach(Array(picks.enumerated()), id: \.offset) { _, pick in + let fat = remote.mealWithFatProtein ? pick.fat : 0 + let protein = remote.mealWithFatProtein ? pick.protein : 0 + let bolus = remote.mealWithBolus && pick.bolus <= remote.maxBolus ? pick.bolus : 0 + Button { + navigator.push(.confirm(request(carbs: pick.carbs, fat: fat, protein: protein, bolus: bolus, offsetMinutes: 0))) + } label: { + Text(pickTitle(carbs: pick.carbs, fat: fat, protein: protein, bolus: bolus)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + private func field(_ label: String, _ id: Field, content: () -> AmountField) -> some View { VStack(alignment: .leading, spacing: 2) { Text(label) @@ -76,6 +102,26 @@ struct MealEntryView: View { } } + private var moreTitle: String { + var parts: [String] = [] + if remote.mealWithFatProtein { parts.append("Fat, Protein") } + if remote.mealWithBolus { parts.append("Bolus") } + parts.append("Time") + return parts.joined(separator: ", ") + } + + private var logTitle: String { + if bolus > 0 { return "Log \(Int(carbs)) g + \(WatchFormat.insulin(bolus)) U" } + return "Log \(Int(carbs)) g" + } + + private func pickTitle(carbs: Double, fat: Double, protein: Double, bolus: Double) -> String { + var parts = ["\(Int(carbs)) g"] + if fat > 0 || protein > 0 { parts.append("F \(Int(fat)) P \(Int(protein))") } + if bolus > 0 { parts.append("\(WatchFormat.insulin(bolus)) U") } + return parts.joined(separator: " · ") + } + private func grams(_ value: Double) -> String { "\(Int(value))" } @@ -86,7 +132,7 @@ struct MealEntryView: View { return m > 0 ? "+\(m)" : "\(m)" } - private var summaryLines: [String] { + private func request(carbs: Double, fat: Double, protein: Double, bolus: Double, offsetMinutes: Double) -> CommandRequest { var lines: [String] = [] if carbs > 0 { lines.append("Carbs \(Int(carbs)) g") } if fat > 0 { lines.append("Fat \(Int(fat)) g") } @@ -94,6 +140,11 @@ struct MealEntryView: View { if bolus > 0 { lines.append("Bolus \(WatchFormat.insulin(bolus)) U") } let at = Date().addingTimeInterval(offsetMinutes * 60) lines.append(offsetMinutes == 0 ? "Now" : at.formatted(date: .omitted, time: .shortened)) - return lines + return CommandRequest( + title: "Meal", + lines: lines, + action: .meal(carbs: carbs, fat: fat, protein: protein, bolus: bolus, consumedAt: at), + deliversInsulin: bolus > 0 + ) } } diff --git a/LoopFollowWatch/Remote/PresetListView.swift b/LoopFollowWatch/Remote/PresetListView.swift index 5eb2a8dee..71846db82 100644 --- a/LoopFollowWatch/Remote/PresetListView.swift +++ b/LoopFollowWatch/Remote/PresetListView.swift @@ -15,7 +15,7 @@ struct PresetListView: View { let state: WatchState let presets: [WatchPreset] - @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var navigator: WatchNavigator private var activeText: String? { guard let snapshot = state.snapshot else { return nil } @@ -33,14 +33,13 @@ struct PresetListView: View { List { if let activeText { Section("Active") { - NavigationLink { - CommandConfirmView( + Button { + navigator.push(.confirm(CommandRequest( title: kind == .override ? "Cancel Override" : "Cancel Temp Target", lines: ["Cancel \(activeText)?"], action: kind == .override ? .cancelOverride : .cancelTempTarget, - deliversInsulin: false, - onSuccess: { dismiss() } - ) + deliversInsulin: false + ))) } label: { Label(activeText, systemImage: "xmark.circle") .foregroundStyle(.red) @@ -55,14 +54,13 @@ struct PresetListView: View { .foregroundStyle(.secondary) } ForEach(presets) { preset in - NavigationLink { - CommandConfirmView( + Button { + navigator.push(.confirm(CommandRequest( title: title, lines: [preset.name, preset.detail], action: kind == .override ? .startOverride(presetID: preset.id) : .startTempTarget(presetID: preset.id), - deliversInsulin: false, - onSuccess: { dismiss() } - ) + deliversInsulin: false + ))) } label: { VStack(alignment: .leading) { Text(preset.name) diff --git a/LoopFollowWatch/Remote/RemotePage.swift b/LoopFollowWatch/Remote/RemotePage.swift deleted file mode 100644 index c21b96470..000000000 --- a/LoopFollowWatch/Remote/RemotePage.swift +++ /dev/null @@ -1,78 +0,0 @@ -// LoopFollow -// RemotePage.swift - -import SwiftUI - -/// Remote commands, laid out like the phone's Remote tab. Every command is sent -/// by the phone, which applies its own guardrails before contacting Loop or Trio. -struct RemotePage: View { - let state: WatchState - let remote: WatchRemoteConfig - @EnvironmentObject private var session: WatchSession - - var body: some View { - ScrollView { - VStack(spacing: 8) { - if !session.isReachable { - Label("iPhone not reachable", systemImage: "iphone.slash") - .font(.footnote) - .foregroundStyle(.orange) - } - LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) { - NavigationLink { - MealEntryView(remote: remote) - } label: { - RemoteTile(title: "Meal", icon: "fork.knife") - } - NavigationLink { - BolusEntryView(state: state, remote: remote) - } label: { - RemoteTile(title: "Bolus", icon: "syringe") - } - if remote.kind == .trio { - NavigationLink { - PresetListView(kind: .tempTarget, state: state, presets: remote.tempTargetPresets) - } label: { - RemoteTile(title: "Temp Target", icon: "scope", isActive: state.snapshot?.tempTargetMgdl != nil) - } - } - NavigationLink { - PresetListView(kind: .override, state: state, presets: remote.overridePresets) - } label: { - RemoteTile(title: "Overrides", icon: "slider.horizontal.3", isActive: state.snapshot?.override != nil) - } - } - .buttonStyle(.plain) - .disabled(!session.isReachable) - .opacity(session.isReachable ? 1 : 0.4) - } - } - .navigationTitle(remote.kind == .trio ? "Trio Remote" : "Loop Remote") - } -} - -/// Same look as the phone's CommandButtonView: blue tile, white icon, green glow while active. -struct RemoteTile: View { - let title: String - let icon: String - var isActive = false - - var body: some View { - VStack(spacing: 4) { - Image(systemName: icon) - .font(.title3) - Text(title) - .font(.footnote) - .lineLimit(1) - .minimumScaleFactor(0.7) - } - .frame(maxWidth: .infinity, minHeight: 64) - .background(Color.blue, in: RoundedRectangle(cornerRadius: 8)) - .foregroundStyle(.white) - .overlay { - if isActive { - RoundedRectangle(cornerRadius: 8).stroke(Color.green, lineWidth: 2) - } - } - } -} diff --git a/LoopFollowWatch/Remote/TreatmentMenuView.swift b/LoopFollowWatch/Remote/TreatmentMenuView.swift new file mode 100644 index 000000000..830648630 --- /dev/null +++ b/LoopFollowWatch/Remote/TreatmentMenuView.swift @@ -0,0 +1,24 @@ +// LoopFollow +// TreatmentMenuView.swift + +import SwiftUI + +struct TreatmentMenuView: View { + @EnvironmentObject private var navigator: WatchNavigator + + var body: some View { + List { + Button { + navigator.push(.meal) + } label: { + Label("Meal", systemImage: "fork.knife") + } + Button { + navigator.push(.bolus) + } label: { + Label("Bolus", systemImage: "syringe") + } + } + .navigationTitle("Treatment") + } +} diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index 15ff3ef24..d8d6107af 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -35,7 +35,9 @@ struct WatchProtocolTests { recommendedBolus: 0.4, recommendedBolusAt: t0, overridePresets: [WatchPreset(id: "Exercise", name: "Exercise", detail: "80 %, 1 h")], - tempTargetPresets: [] + tempTargetPresets: [], + quickPickBoluses: [1, 2.5], + quickPickMeals: [WatchMealPick(carbs: 45, fat: 0, protein: 0, bolus: 0)] ) ) } diff --git a/WatchShared/WatchCommand.swift b/WatchShared/WatchCommand.swift index afda403ba..96f932563 100644 --- a/WatchShared/WatchCommand.swift +++ b/WatchShared/WatchCommand.swift @@ -10,7 +10,7 @@ struct WatchCommand: Codable, Equatable { /// The phone ignores commands older than this, so a delayed delivery is never acted on. static let maxAge: TimeInterval = 60 - enum Action: Codable, Equatable { + enum Action: Codable, Hashable { case bolus(units: Double) /// Loop sends carbs only; Trio may add fat, protein and a bolus in the same command. case meal(carbs: Double, fat: Double, protein: Double, bolus: Double, consumedAt: Date) diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index f44e1b653..d76517312 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -109,4 +109,14 @@ struct WatchRemoteConfig: Codable, Equatable { let overridePresets: [WatchPreset] /// Trio only. let tempTargetPresets: [WatchPreset] + /// The phone's Quick-Pick suggestions for this time of day. + let quickPickBoluses: [Double] + let quickPickMeals: [WatchMealPick] +} + +struct WatchMealPick: Codable, Equatable { + let carbs: Double + let fat: Double + let protein: Double + let bolus: Double } From eb38244cdc4eac0eefcf971160fed6bd1601b63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 13:25:40 +0200 Subject: [PATCH 04/25] Point browser builders to the docs when the watch identifiers are not set up --- fastlane/Fastfile | 85 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 88a852749..f99a56188 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -45,6 +45,31 @@ APP_IDENTIFIER = "com.#{TEAMID}.LoopFollow#{APP_SUFFIX}" LA_IDENTIFIER = "#{APP_IDENTIFIER}.LoopFollowLAExtension" WATCH_IDENTIFIER = "#{APP_IDENTIFIER}.watchkitapp" WATCH_WIDGETS_IDENTIFIER = "#{WATCH_IDENTIFIER}.widgets" +WATCH_SETUP_DOCS = "https://loopfollowdocs.org/build/lf-browser-build/#update-for-the-apple-watch-app" + +# The watch app needs two identifiers with the LoopFollow App Group, which +# existing browser builders have to add once by hand. Turns the resulting +# signing failures into a pointer to the docs. +def explain_watch_setup_failure(error, log_dir = nil) + text = error.message.to_s + if log_dir + roots = [log_dir, File.join("..", log_dir), File.join(GITHUB_WORKSPACE.to_s, log_dir)].uniq + roots.flat_map { |root| Dir.glob(File.join(root, "**", "*.log")) }.uniq.each do |log| + text += File.read(log) rescue nil + end + end + + mentions_watch = [WATCH_IDENTIFIER, WATCH_WIDGETS_IDENTIFIER, "LoopFollowWatch"].any? { |id| text.include?(id) } + missing_identifier = text.match?(/(couldn't|could not|unable to) find|not found|does(n't| not) exist/i) + missing_app_group = text.match?(/App Groups capability|com\.apple\.security\.application-groups/i) + return unless mentions_watch && (missing_identifier || missing_app_group) + + UI.user_error!( + "The Apple Watch app identifiers are not set up yet. Run the Add Identifiers action, " \ + "add the LoopFollow App Group to the LoopFollow Watch App and LoopFollow Watch Complications identifiers, " \ + "then build again. Instructions: #{WATCH_SETUP_DOCS}" + ) +end platform :ios do desc "Build Loop Follow" @@ -74,16 +99,21 @@ platform :ios do build_number: current_build_number ) - match( - type: "appstore", - git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), - app_identifier: [ - APP_IDENTIFIER, - LA_IDENTIFIER, - WATCH_IDENTIFIER, - WATCH_WIDGETS_IDENTIFIER - ] - ) + begin + match( + type: "appstore", + git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), + app_identifier: [ + APP_IDENTIFIER, + LA_IDENTIFIER, + WATCH_IDENTIFIER, + WATCH_WIDGETS_IDENTIFIER + ] + ) + rescue => e + explain_watch_setup_failure(e) + raise + end mapping = Actions.lane_context[ SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING @@ -117,22 +147,27 @@ platform :ios do targets: ["LoopFollowWatchWidgets"] ) - gym( - export_method: "app-store", - scheme: "LoopFollow", - output_name: "LoopFollow.ipa", - configuration: "Release", - destination: 'generic/platform=iOS', - buildlog_path: 'buildlog', - export_options: { - provisioningProfiles: { - APP_IDENTIFIER => mapping[APP_IDENTIFIER], - LA_IDENTIFIER => mapping[LA_IDENTIFIER], - WATCH_IDENTIFIER => mapping[WATCH_IDENTIFIER], - WATCH_WIDGETS_IDENTIFIER => mapping[WATCH_WIDGETS_IDENTIFIER] + begin + gym( + export_method: "app-store", + scheme: "LoopFollow", + output_name: "LoopFollow.ipa", + configuration: "Release", + destination: 'generic/platform=iOS', + buildlog_path: 'buildlog', + export_options: { + provisioningProfiles: { + APP_IDENTIFIER => mapping[APP_IDENTIFIER], + LA_IDENTIFIER => mapping[LA_IDENTIFIER], + WATCH_IDENTIFIER => mapping[WATCH_IDENTIFIER], + WATCH_WIDGETS_IDENTIFIER => mapping[WATCH_WIDGETS_IDENTIFIER] + } } - } - ) + ) + rescue => e + explain_watch_setup_failure(e, "buildlog") + raise + end copy_artifacts( target_path: "artifacts", From 89420cc89e2749aae5568ea308b64e9c9b6af992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 13:33:40 +0200 Subject: [PATCH 05/25] Keep watch Info values clear of the page indicator and widen the Meal buttons --- LoopFollowWatch/InfoPage.swift | 2 ++ LoopFollowWatch/Remote/MealEntryView.swift | 3 +++ 2 files changed, 5 insertions(+) diff --git a/LoopFollowWatch/InfoPage.swift b/LoopFollowWatch/InfoPage.swift index f18b32962..d0e4b596e 100644 --- a/LoopFollowWatch/InfoPage.swift +++ b/LoopFollowWatch/InfoPage.swift @@ -12,6 +12,8 @@ struct InfoPage: View { VStack(spacing: 0) { rows } + // Keeps the values clear of the page indicator. + .padding(.trailing, 10) } .navigationTitle("Info") } diff --git a/LoopFollowWatch/Remote/MealEntryView.swift b/LoopFollowWatch/Remote/MealEntryView.swift index c64175277..ec2d1fea8 100644 --- a/LoopFollowWatch/Remote/MealEntryView.swift +++ b/LoopFollowWatch/Remote/MealEntryView.swift @@ -55,7 +55,9 @@ struct MealEntryView: View { } label: { Label(moreTitle, systemImage: "ellipsis.circle") .font(.footnote) + .frame(maxWidth: .infinity, alignment: .leading) } + .frame(maxWidth: .infinity) } Button { @@ -88,6 +90,7 @@ struct MealEntryView: View { Text(pickTitle(carbs: pick.carbs, fat: fat, protein: protein, bolus: bolus)) .frame(maxWidth: .infinity, alignment: .leading) } + .frame(maxWidth: .infinity) } } } From 96961e460eae6baf25348b42c23f3f3fea277cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 14:09:08 +0200 Subject: [PATCH 06/25] Check that the watch identifiers exist before match, since its read-only failure does not name the missing profile --- fastlane/Fastfile | 54 +++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f99a56188..8bf9986ce 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -48,8 +48,22 @@ WATCH_WIDGETS_IDENTIFIER = "#{WATCH_IDENTIFIER}.widgets" WATCH_SETUP_DOCS = "https://loopfollowdocs.org/build/lf-browser-build/#update-for-the-apple-watch-app" # The watch app needs two identifiers with the LoopFollow App Group, which -# existing browser builders have to add once by hand. Turns the resulting -# signing failures into a pointer to the docs. +# existing browser builders have to add once by hand. +def watch_setup_error! + UI.user_error!( + "The Apple Watch app identifiers are not set up yet. Run the Add Identifiers action, " \ + "add the LoopFollow App Group to the LoopFollow Watch App and LoopFollow Watch Complications identifiers, " \ + "then build again. Instructions: #{WATCH_SETUP_DOCS}" + ) +end + +# Stops before match, whose read-only failure does not name the missing profile. +def ensure_watch_identifiers_exist + missing = [WATCH_IDENTIFIER, WATCH_WIDGETS_IDENTIFIER].reject { |id| Spaceship::ConnectAPI::BundleId.find(id) } + watch_setup_error! unless missing.empty? +end + +# Turns a signing failure caused by a watch identifier without the App Group into a pointer to the docs. def explain_watch_setup_failure(error, log_dir = nil) text = error.message.to_s if log_dir @@ -60,15 +74,8 @@ def explain_watch_setup_failure(error, log_dir = nil) end mentions_watch = [WATCH_IDENTIFIER, WATCH_WIDGETS_IDENTIFIER, "LoopFollowWatch"].any? { |id| text.include?(id) } - missing_identifier = text.match?(/(couldn't|could not|unable to) find|not found|does(n't| not) exist/i) missing_app_group = text.match?(/App Groups capability|com\.apple\.security\.application-groups/i) - return unless mentions_watch && (missing_identifier || missing_app_group) - - UI.user_error!( - "The Apple Watch app identifiers are not set up yet. Run the Add Identifiers action, " \ - "add the LoopFollow App Group to the LoopFollow Watch App and LoopFollow Watch Complications identifiers, " \ - "then build again. Instructions: #{WATCH_SETUP_DOCS}" - ) + watch_setup_error! if mentions_watch && missing_app_group end platform :ios do @@ -99,21 +106,18 @@ platform :ios do build_number: current_build_number ) - begin - match( - type: "appstore", - git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), - app_identifier: [ - APP_IDENTIFIER, - LA_IDENTIFIER, - WATCH_IDENTIFIER, - WATCH_WIDGETS_IDENTIFIER - ] - ) - rescue => e - explain_watch_setup_failure(e) - raise - end + ensure_watch_identifiers_exist + + match( + type: "appstore", + git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), + app_identifier: [ + APP_IDENTIFIER, + LA_IDENTIFIER, + WATCH_IDENTIFIER, + WATCH_WIDGETS_IDENTIFIER + ] + ) mapping = Actions.lane_context[ SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING From b27b8543a8cfd7cbda49e69b1cb73fddda72dfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 14:59:19 +0200 Subject: [PATCH 07/25] Mirror the phone's info table on the watch: same rows, order and coloring --- LoopFollow/InfoTable/InfoDisplayItem.swift | 20 +++- .../ViewControllers/MainViewController.swift | 1 + LoopFollow/Watch/WatchSessionManager.swift | 9 ++ LoopFollow/Watch/WatchStateBuilder.swift | 24 ++++ LoopFollowWatch/InfoPage.swift | 103 +++++------------- Tests/WatchProtocolTests.swift | 1 + WatchShared/WatchState.swift | 16 +++ 7 files changed, 94 insertions(+), 80 deletions(-) diff --git a/LoopFollow/InfoTable/InfoDisplayItem.swift b/LoopFollow/InfoTable/InfoDisplayItem.swift index cf3a3c1b9..8e8da861a 100644 --- a/LoopFollow/InfoTable/InfoDisplayItem.swift +++ b/LoopFollow/InfoTable/InfoDisplayItem.swift @@ -59,15 +59,25 @@ struct InfoColoring: Codable, Equatable { /// the default text color); when enabled, an in-range value reads green, /// mirroring the app's BG text coloring. func color(for value: Double, direction: InfoColorDirection) -> Color? { + switch level(for: value, direction: direction) { + case .urgent: .red + case .warning: .yellow + case .normal: .green + case nil: nil + } + } + + /// The threshold band a value falls in, or `nil` when coloring is off. + func level(for value: Double, direction: InfoColorDirection) -> InfoColorLevel? { guard enabled else { return nil } switch direction { case .above: - if let urgent, value >= urgent { return .red } - if let warning, value >= warning { return .yellow } + if let urgent, value >= urgent { return .urgent } + if let warning, value >= warning { return .warning } case .below: - if let urgent, value <= urgent { return .red } - if let warning, value <= warning { return .yellow } + if let urgent, value <= urgent { return .urgent } + if let warning, value <= warning { return .warning } } - return .green + return .normal } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 5c6fb7e4f..5712ffc5b 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -152,6 +152,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { view.backgroundColor = .systemBackground infoManager = InfoManager() + WatchSessionManager.shared.observe(infoManager) let mainView = MainHomeView( chartModel: chartModel, diff --git a/LoopFollow/Watch/WatchSessionManager.swift b/LoopFollow/Watch/WatchSessionManager.swift index dc841e40e..70619ff99 100644 --- a/LoopFollow/Watch/WatchSessionManager.swift +++ b/LoopFollow/Watch/WatchSessionManager.swift @@ -52,6 +52,8 @@ final class WatchSessionManager: NSObject { Storage.shared.units.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.mealWithBolus.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.mealWithFatProtein.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.infoDisplayItems.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.graphTimeZoneEnabled.$value.map { _ in () }.eraseToAnyPublisher(), ] Publishers.MergeMany(triggers) .dropFirst(triggers.count) @@ -59,6 +61,13 @@ final class WatchSessionManager: NSObject { .store(in: &cancellables) } + /// Sends the info table to the watch whenever one of its rows changes. + func observe(_ infoManager: InfoManager) { + infoManager.objectWillChange + .sink { [weak self] in self?.requestUpdate(reason: "info") } + .store(in: &cancellables) + } + private func publish(reason: String) { guard let session, session.activationState == .activated else { return } let state = WatchStateBuilder.build() diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index baac97fa7..c4d1b38bc 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -58,6 +58,7 @@ enum WatchStateBuilder { lowMgdl: thresholds.low, highMgdl: thresholds.high, colorBGText: Storage.shared.colorBGText.value, + infoRows: infoRows(vc), lastLoopAt: lastLoop, device: Storage.shared.device.value, alarm: currentAlarm(), @@ -65,6 +66,29 @@ enum WatchStateBuilder { ) } + /// Same rows, order and coloring as the phone's info table (InfoTableView). + private static func infoRows(_ vc: MainViewController?) -> [WatchInfoRow] { + var rows: [WatchInfoRow] = [] + if Storage.shared.graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) + { + rows.append(WatchInfoRow(name: "Time Zone", value: tz.identifier, level: nil)) + } + guard let infoManager = vc?.infoManager else { return rows } + let items = Storage.shared.infoDisplayItems.value + for row in infoManager.visibleRows { + var level: InfoColorLevel? + if let numeric = row.numericValue, + let type = InfoType(rawValue: row.id), + let config = type.colorConfig + { + level = items.item(for: type)?.coloring.level(for: numeric, direction: config.direction) + } + rows.append(WatchInfoRow(name: row.name, value: row.value.isEmpty ? "—" : row.value, level: level)) + } + return rows + } + private static func currentAlarm() -> WatchAlarm? { guard let id = Observable.shared.currentAlarm.value, let alarm = Storage.shared.alarms.value.first(where: { $0.id == id }) diff --git a/LoopFollowWatch/InfoPage.swift b/LoopFollowWatch/InfoPage.swift index d0e4b596e..c5f1a3a71 100644 --- a/LoopFollowWatch/InfoPage.swift +++ b/LoopFollowWatch/InfoPage.swift @@ -3,14 +3,33 @@ import SwiftUI -/// A compact table like the phone's info table; rows without data are hidden. +/// The phone's info table: the same rows, order and threshold coloring. struct InfoPage: View { let state: WatchState var body: some View { ScrollView { VStack(spacing: 0) { - rows + if state.infoRows.isEmpty { + Text("No info rows are shown on the iPhone.") + .font(.footnote) + .foregroundStyle(.secondary) + } + ForEach(Array(state.infoRows.enumerated()), id: \.offset) { _, row in + HStack { + Text(row.name) + .foregroundStyle(.secondary) + Spacer() + Text(row.value) + .foregroundStyle(Self.color(row.level)) + .multilineTextAlignment(.trailing) + } + .font(.footnote) + .lineLimit(1) + .minimumScaleFactor(0.6) + .padding(.vertical, 3) + Divider() + } } // Keeps the values clear of the page indicator. .padding(.trailing, 10) @@ -18,79 +37,13 @@ struct InfoPage: View { .navigationTitle("Info") } - @ViewBuilder - private var rows: some View { - if let s = state.snapshot { - row("IOB", s.iob.map { WatchFormat.iob($0) }) - row("COB", s.cob.map { WatchFormat.cob($0) }) - row("Rec. Bolus", s.recBolus.map { "\(WatchFormat.insulin($0)) U" }) - row("Basal", s.basalRate.isEmpty ? nil : s.basalRate) - row("Projected", s.projected.map { WatchFormat.glucose($0, unit: s.unit) }) - row("Min/Max", minMax(s)) - row("Override", s.override.map { name in name + remaining(s.overrideEndAt) }) - row("Temp Target", s.tempTargetMgdl.map { WatchFormat.glucose($0, unit: s.unit) + remaining(s.tempTargetEndAt) }) - row("Target", target(s)) - row("ISF", s.isfMgdlPerU.map { WatchFormat.glucose($0, unit: s.unit) }) - row("CR", s.carbRatio.map { "\(WatchFormat.insulin($0)) g" }) - row("Autosens", s.autosens.map { "\(Int(round($0 * 100))) %" }) - row("TDD", s.tdd.map { "\(WatchFormat.insulin($0)) U" }) - row("Carbs today", s.carbsToday.map { "\(Int(round($0))) g" }) - row("Pump", s.pumpReservoirU.map { "\(WatchFormat.insulin($0)) U" }) - row("Pump Battery", s.pumpBattery.map { "\(Int(round($0))) %" }) - row("Battery", s.battery.map { "\(Int(round($0))) %" }) - row("SAGE", age(s.sageInsertTime)) - row("CAGE", age(s.cageInsertTime)) - row("IAGE", age(s.iageInsertTime)) - row("Profile", s.profileName) + /// Same colors as the phone's InfoColoring. + static func color(_ level: InfoColorLevel?) -> Color { + switch level { + case .urgent: .red + case .warning: .yellow + case .normal: .green + case nil: .primary } - row("Last Loop", state.lastLoopAt.map { WatchFormat.minutesAgo($0) + " ago" }) - if let sensor = state.snapshot?.cgmSensorState { - Text(sensor) - .font(.footnote) - .foregroundStyle(.orange) - .padding(.top, 4) - } - } - - @ViewBuilder - private func row(_ label: String, _ value: String?) -> some View { - if let value { - HStack { - Text(label) - .foregroundStyle(.secondary) - Spacer() - Text(value) - .multilineTextAlignment(.trailing) - } - .font(.footnote) - .padding(.vertical, 3) - Divider() - } - } - - private func minMax(_ s: GlucoseSnapshot) -> String? { - guard let low = s.minBgMgdl, let high = s.maxBgMgdl else { return nil } - return "\(WatchFormat.glucose(low, unit: s.unit))/\(WatchFormat.glucose(high, unit: s.unit))" - } - - private func target(_ s: GlucoseSnapshot) -> String? { - guard let low = s.targetLowMgdl else { return nil } - let lowText = WatchFormat.glucose(low, unit: s.unit) - guard let high = s.targetHighMgdl, round(high) != round(low) else { return lowText } - return "\(lowText)–\(WatchFormat.glucose(high, unit: s.unit))" - } - - private func remaining(_ endAt: TimeInterval?) -> String { - guard let endAt else { return "" } - let minutes = Int((endAt - Date().timeIntervalSince1970) / 60) - guard minutes > 0 else { return "" } - return minutes >= 60 ? " (\(minutes / 60) h \(minutes % 60) min)" : " (\(minutes) min)" - } - - private func age(_ insertTime: TimeInterval) -> String? { - guard insertTime > 0 else { return nil } - let hours = Int((Date().timeIntervalSince1970 - insertTime) / 3600) - guard hours >= 0 else { return nil } - return "\(hours / 24)d \(hours % 24)h" } } diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index d8d6107af..abd1176e2 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -20,6 +20,7 @@ struct WatchProtocolTests { lowMgdl: 70, highMgdl: 180, colorBGText: true, + infoRows: [WatchInfoRow(name: "IOB", value: "1,2", level: nil), WatchInfoRow(name: "SAGE", value: "9d 2h", level: .warning)], lastLoopAt: t0, device: "Trio", alarm: alarm, diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index d76517312..2bc8b4019 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -26,6 +26,8 @@ struct WatchState: Codable, Equatable { var highMgdl: Double /// Mirrors the phone's "Color BG Text" setting. var colorBGText: Bool + /// The phone's visible info table rows, in its order and with its coloring. + var infoRows: [WatchInfoRow] /// When the loop last reported, nil when unknown. var lastLoopAt: Date? /// "Loop", "Trio" or another uploader name; empty for CGM-only setups. @@ -53,6 +55,20 @@ struct WatchReading: Codable, Equatable { var date: Date { Date(timeIntervalSince1970: t) } } +struct WatchInfoRow: Codable, Equatable { + let name: String + let value: String + /// Threshold coloring from the phone's Info Display settings; nil uses the default text color. + let level: InfoColorLevel? +} + +/// Threshold band of an info table value, shared by the phone and the watch. +enum InfoColorLevel: String, Codable { + case normal + case warning + case urgent +} + struct WatchPredictionLine: Codable, Equatable { /// Matches the phone's chart series and colors. enum Kind: String, Codable { From e3bc6b226be48ea3e3990f7efc1d95649a0c761e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:01:50 +0200 Subject: [PATCH 08/25] Share the APNs, JWT, TRC encryption and Loop payload code between the phone and the watch --- LoopFollow.xcodeproj/project.pbxproj | 10 + .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 26 -- .../Remote/LoopAPNS/LoopAPNSService.swift | 232 ++---------------- .../Remote/TRC/PushNotificationManager.swift | 94 +------ LoopFollowWatch/LogManager.swift | 32 +++ RemoteShared/APNSDelivery.swift | 100 ++++++++ .../APNSEnvironment.swift | 0 .../Helpers => RemoteShared}/JWTManager.swift | 0 .../LogRedactor.swift | 0 RemoteShared/LoopAPNSCommands.swift | 121 +++++++++ .../OTPSecureMessenger.swift | 0 .../TRC => RemoteShared}/PushMessage.swift | 0 .../SecureMessenger.swift | 0 .../TOTPGenerator.swift | 0 .../TRC => RemoteShared}/TRCCommandType.swift | 0 Tests/LoopAPNSCommandPayloadTests.swift | 10 +- 16 files changed, 304 insertions(+), 321 deletions(-) create mode 100644 LoopFollowWatch/LogManager.swift create mode 100644 RemoteShared/APNSDelivery.swift rename {LoopFollow/Remote => RemoteShared}/APNSEnvironment.swift (100%) rename {LoopFollow/Helpers => RemoteShared}/JWTManager.swift (100%) rename {LoopFollow/Helpers => RemoteShared}/LogRedactor.swift (100%) create mode 100644 RemoteShared/LoopAPNSCommands.swift rename {LoopFollow/Remote/LoopAPNS => RemoteShared}/OTPSecureMessenger.swift (100%) rename {LoopFollow/Remote/TRC => RemoteShared}/PushMessage.swift (100%) rename {LoopFollow/Remote/TRC => RemoteShared}/SecureMessenger.swift (100%) rename {LoopFollow/Helpers => RemoteShared}/TOTPGenerator.swift (100%) rename {LoopFollow/Remote/TRC => RemoteShared}/TRCCommandType.swift (100%) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index c9c5d2091..9397123bb 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -219,6 +219,13 @@ path = WatchShared; sourceTree = ""; }; + DD57A7C02F70000000000101 /* RemoteShared */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = RemoteShared; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -314,6 +321,7 @@ DD57A7C02F7000000000000F /* LoopFollowWatch */, DD57A7C02F70000000000010 /* LoopFollowWatchWidgets */, DD57A7C02F70000000000011 /* WatchShared */, + DD57A7C02F70000000000101 /* RemoteShared */, ); sourceTree = ""; }; @@ -398,6 +406,7 @@ 9555FE45F7FAEFA6142D04CA /* LoopFollow */, BC0FBFD3754955D028EF6631 /* Shared */, DD57A7C02F70000000000011 /* WatchShared */, + DD57A7C02F70000000000101 /* RemoteShared */, ); name = LoopFollow; packageProductDependencies = ( @@ -425,6 +434,7 @@ DD57A7C02F7000000000000F /* LoopFollowWatch */, DD57A7C02F70000000000011 /* WatchShared */, BC0FBFD3754955D028EF6631 /* Shared */, + DD57A7C02F70000000000101 /* RemoteShared */, ); name = LoopFollowWatch; productName = LoopFollowWatch; diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 537fa29d2..1392e697e 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -623,29 +623,3 @@ struct LoopAPNSCarbsView: View { min(max(date, oldestAcceptedDate), latestAcceptedDate) } } - -// APNS Payload structure for carbs -struct LoopAPNSPayload { - enum PayloadType { - case carbs - case bolus - } - - let type: PayloadType - let carbsAmount: Double? - let absorptionTime: Double? - let foodType: String? - let bolusAmount: Double? - let consumedDate: Date? - let otp: String - - init(type: PayloadType, carbsAmount: Double? = nil, absorptionTime: Double? = nil, foodType: String? = nil, bolusAmount: Double? = nil, consumedDate: Date? = nil, otp: String) { - self.type = type - self.carbsAmount = carbsAmount - self.absorptionTime = absorptionTime - self.foodType = foodType - self.bolusAmount = bolusAmount - self.consumedDate = consumedDate - self.otp = otp - } -} diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift index 2ca317809..658986db1 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift @@ -124,73 +124,6 @@ class LoopAPNSService { private static let returnNotificationRequiredMessage = "Editing or deleting a carb entry needs LoopFollow's own APNS credentials so Loop can confirm the result. Configure them in App Settings → APN." - // MARK: - Command payloads (matching Nightscout's loop.js format) - - /// Fields every Loop remote command carries. - static func commandHeader(otp: String, alert: String, now: Date) -> [String: Any] { - [ - "otp": otp, - "remote-address": "LoopFollow", - "notes": "Sent via LoopFollow APNS", - "entered-by": "LoopFollow", - "sent-at": formatDateForAPNS(now), - "expiration": formatDateForAPNS(now.addingTimeInterval(5 * 60)), - "alert": alert, - ] - } - - static func carbsCommandPayload(_ payload: LoopAPNSPayload, now: Date = Date()) -> [String: Any] { - let carbsAmount = payload.carbsAmount ?? 0.0 - let absorptionTime = payload.absorptionTime ?? 3.0 - var fields = commandHeader( - otp: payload.otp, - alert: "Remote Carbs Entry: \(String(format: "%.1f", carbsAmount)) grams\nAbsorption Time: \(String(format: "%.1f", absorptionTime)) hours", - now: now - ) - fields["carbs-entry"] = carbsAmount - fields["absorption-time"] = absorptionTime - fields["start-time"] = formatDateForAPNS(payload.consumedDate ?? now) - return fields - } - - static func bolusCommandPayload(_ payload: LoopAPNSPayload, now: Date = Date()) -> [String: Any] { - let bolusAmount = payload.bolusAmount ?? 0.0 - var fields = commandHeader(otp: payload.otp, alert: "Remote Bolus Entry: \(String(format: "%.2f", bolusAmount)) U", now: now) - fields["bolus-entry"] = bolusAmount - return fields - } - - static func carbsDeleteCommandPayload(syncIdentifier: String, otp: String, now: Date = Date()) -> [String: Any] { - var fields = commandHeader(otp: otp, alert: "Remote Carbs Delete", now: now) - fields["carbs-delete"] = syncIdentifier - return fields - } - - /// Keys are prefixed so an unpatched Loop rejects the command. Amount, absorption time and start time are always sent; food type only when set. - static func carbsEditCommandPayload( - syncIdentifier: String, - carbsAmount: Double, - absorptionTimeHours: Double, - foodType: String?, - consumedDate: Date, - otp: String, - now: Date = Date() - ) -> [String: Any] { - var fields = commandHeader( - otp: otp, - alert: "Remote Carbs Edit: \(String(format: "%.1f", carbsAmount)) grams\nAbsorption Time: \(String(format: "%.1f", absorptionTimeHours)) hours", - now: now - ) - fields["carbs-edit"] = syncIdentifier - fields["carbs-edit-entry"] = carbsAmount - fields["carbs-edit-absorption-time"] = absorptionTimeHours - fields["carbs-edit-start-time"] = formatDateForAPNS(consumedDate) - if let foodType, !foodType.isEmpty { - fields["carbs-edit-food-type"] = foodType - } - return fields - } - /// The return-notification block encrypted with the OTP, when LoopFollow's own credentials are configured. private func encryptedReturnNotification(otp: String) -> String? { guard let returnInfo = createReturnNotificationInfo() else { @@ -233,7 +166,7 @@ class LoopAPNSService { /// Deletes the Loop carb entry with this `syncIdentifier`. Requires the Loop remote carb edit patch. func sendCarbsDelete(syncIdentifier: String, otp: String, completion: @escaping (Bool, String?) -> Void) { LogManager.shared.log(category: .apns, message: "Sending carbs delete for syncIdentifier=\(LogRedactor.tail(syncIdentifier))") - sendConfirmedCommand(Self.carbsDeleteCommandPayload(syncIdentifier: syncIdentifier, otp: otp), otp: otp, completion: completion) + sendConfirmedCommand(LoopAPNSCommands.carbsDeleteCommandPayload(syncIdentifier: syncIdentifier, otp: otp), otp: otp, completion: completion) } /// Replaces the Loop carb entry with this `syncIdentifier`. Requires the Loop remote carb edit patch. @@ -247,7 +180,7 @@ class LoopAPNSService { completion: @escaping (Bool, String?) -> Void ) { LogManager.shared.log(category: .apns, message: "Sending carbs edit for syncIdentifier=\(LogRedactor.tail(syncIdentifier)): \(String(format: "%.1f", carbsAmount))g, absorption \(String(format: "%.1f", absorptionTimeHours))h") - let payload = Self.carbsEditCommandPayload( + let payload = LoopAPNSCommands.carbsEditCommandPayload( syncIdentifier: syncIdentifier, carbsAmount: carbsAmount, absorptionTimeHours: absorptionTimeHours, @@ -263,7 +196,7 @@ class LoopAPNSService { /// - payload: The carbs payload to send /// - completion: Completion handler with success status and error message func sendCarbsViaAPNS(payload: LoopAPNSPayload, completion: @escaping (Bool, String?) -> Void) { - var finalPayload = Self.carbsCommandPayload(payload) + var finalPayload = LoopAPNSCommands.carbsCommandPayload(payload) if let encryptedReturnInfo = encryptedReturnNotification(otp: payload.otp) { finalPayload["encrypted_return_notification"] = encryptedReturnInfo } @@ -276,7 +209,7 @@ class LoopAPNSService { /// - payload: The bolus payload to send /// - completion: Completion handler with success status and error message func sendBolusViaAPNS(payload: LoopAPNSPayload, completion: @escaping (Bool, String?) -> Void) { - var finalPayload = Self.bolusCommandPayload(payload) + var finalPayload = LoopAPNSCommands.bolusCommandPayload(payload) if let encryptedReturnInfo = encryptedReturnNotification(otp: payload.otp) { finalPayload["encrypted_return_notification"] = encryptedReturnInfo } @@ -359,145 +292,37 @@ class LoopAPNSService { return } - // Create JWT token for APNS authentication - guard let jwt = JWTManager.shared.getOrGenerateJWT(keyId: keyId, teamId: teamId, apnsKey: apnsKey) else { - let errorMessage = "Failed to generate JWT, please check that the APNS Key ID, APNS Key and Team ID are correct." - LogManager.shared.log(category: .apns, message: errorMessage) - completion(false, errorMessage) - return - } - - // Determine APNS environment - let isProduction = storage.productionEnvironment.value - let apnsURL = APNSEnvironment.baseURL(production: isProduction) - guard let requestURL = URL(string: "\(apnsURL)/3/device/\(deviceToken)") else { - let errorMessage = "Failed to construct APNs URL" - LogManager.shared.log(category: .apns, message: errorMessage) - completion(false, errorMessage) - return - } - - var request = URLRequest(url: requestURL) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "content-type") - request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization") - request.setValue(bundleIdentifier, forHTTPHeaderField: "apns-topic") - request.setValue("alert", forHTTPHeaderField: "apns-push-type") - request.setValue("10", forHTTPHeaderField: "apns-priority") // High priority - // Validate bundle identifier format if !bundleIdentifier.contains(".") { LogManager.shared.log(category: .apns, message: "Warning: Bundle identifier may be in wrong format: \(bundleIdentifier)") } - // Create the proper APNS payload structure (matching @parse/node-apn format) - var apnsPayload: [String: Any] = [ - "aps": [ - "alert": payload["alert"] as? String ?? "", - "content-available": 1, - "interruption-level": "time-sensitive", - ], - ] - - // Add all the custom payload fields (excluding APNS-specific fields) - for (key, value) in payload { - if key != "alert", key != "content-available", key != "interruption-level" { - apnsPayload[key] = value - } - } - - // Remove nil values to clean up the payload - let cleanPayload = apnsPayload.compactMapValues { $0 } - - // Log if encrypted_return_notification is in the payload - if cleanPayload["encrypted_return_notification"] != nil { + if payload["encrypted_return_notification"] != nil { LogManager.shared.log(category: .apns, message: "encrypted_return_notification is present in final APNS payload") } else { - LogManager.shared.log(category: .apns, message: "WARNING: encrypted_return_notification is NOT in final APNS payload. Available keys: \(Array(cleanPayload.keys).joined(separator: ", "))") + LogManager.shared.log(category: .apns, message: "WARNING: encrypted_return_notification is NOT in final APNS payload. Available keys: \(Array(payload.keys).joined(separator: ", "))") } + let body: Data do { - let jsonData = try JSONSerialization.data(withJSONObject: cleanPayload) - - request.httpBody = jsonData - - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - let errorMessage = "Failed to send push notification: \(error.localizedDescription)" - LogManager.shared.log(category: .apns, message: errorMessage) - completion(false, errorMessage) - return - } - - if let httpResponse = response as? HTTPURLResponse { - var responseBodyMessage = "" - if let data = data, let responseBody = String(data: data, encoding: .utf8) { - if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let reason = json["reason"] as? String - { - responseBodyMessage = reason - } - } - - switch httpResponse.statusCode { - case 200: - LogManager.shared.log(category: .apns, message: "APNS notification sent successfully") - completion(true, nil) - case 400: - let errorMessage = "Bad request. The request was invalid or malformed. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 400: \(responseBodyMessage) - Check device token and environment settings") - completion(false, errorMessage) - case 403: - JWTManager.shared.invalidateCache() - let errorMessage = "Authentication error. Check your certificate or authentication token. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 403: \(responseBodyMessage) - Check APNS key permissions for bundle ID") - completion(false, errorMessage) - case 404: - let errorMessage = "Invalid request: The :path value was incorrect. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 404: \(responseBodyMessage)") - completion(false, errorMessage) - case 405: - let errorMessage = "Invalid request: Only POST requests are supported. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 405: \(responseBodyMessage)") - completion(false, errorMessage) - case 410: - let errorMessage = "The device token is no longer active for the topic. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 410: Device token is invalid or expired") - completion(false, errorMessage) - case 413: - let errorMessage = "Payload too large. The notification payload exceeded the size limit. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 413: \(responseBodyMessage)") - completion(false, errorMessage) - case 429: - let errorMessage = "Too many requests. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 429: Rate limited - wait before retrying") - completion(false, errorMessage) - case 500: - let errorMessage = "Internal server error at APNs. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 500: \(responseBodyMessage)") - completion(false, errorMessage) - case 503: - let errorMessage = "Service unavailable. The server is temporarily unavailable. Try again later. \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error 503: \(responseBodyMessage)") - completion(false, errorMessage) - default: - let errorMessage = "Unexpected status code: \(httpResponse.statusCode). \(responseBodyMessage)" - LogManager.shared.log(category: .apns, message: "APNS error \(httpResponse.statusCode): \(responseBodyMessage)") - completion(false, errorMessage) - } - } else { - let errorMessage = "Failed to get a valid HTTP response." - LogManager.shared.log(category: .apns, message: errorMessage) - completion(false, errorMessage) - } - } - task.resume() - + body = try LoopAPNSCommands.apnsBody(payload) } catch { let errorMessage = "Failed to serialize APNS payload: \(error.localizedDescription)" LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) + return } + + APNSDelivery.send( + body: body, + deviceToken: deviceToken, + topic: bundleIdentifier, + production: storage.productionEnvironment.value, + keyId: keyId, + teamId: teamId, + apnsKey: apnsKey, + completion: completion + ) } /// Validates and fixes APNS key format if needed @@ -648,15 +473,6 @@ class LoopAPNSService { // MARK: - Date Formatting Helper - /// Creates a properly formatted ISO8601 date string with milliseconds (matching Nightscout's format) - /// - Parameter date: The date to format - /// - Returns: Formatted date string like "2022-12-24T21:34:02.090Z" - private static func formatDateForAPNS(_ date: Date) -> String { - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return dateFormatter.string(from: date) - } - // MARK: - Override Methods func sendOverrideNotification(presetName: String, duration: TimeInterval? = nil, completion: @escaping (Bool, String?) -> Void) { @@ -696,8 +512,8 @@ class LoopAPNSService { "override-name": presetName, "remote-address": "LoopFollow", "entered-by": "LoopFollow", - "sent-at": Self.formatDateForAPNS(now), - "expiration": Self.formatDateForAPNS(expiration), + "sent-at": LoopAPNSCommands.formatDateForAPNS(now), + "expiration": LoopAPNSCommands.formatDateForAPNS(expiration), "alert": alertText, ] @@ -756,8 +572,8 @@ class LoopAPNSService { "cancel-temporary-override": "true", "remote-address": "LoopFollow", "entered-by": "LoopFollow", - "sent-at": Self.formatDateForAPNS(now), - "expiration": Self.formatDateForAPNS(expiration), + "sent-at": LoopAPNSCommands.formatDateForAPNS(now), + "expiration": LoopAPNSCommands.formatDateForAPNS(expiration), "alert": "Cancel Temporary Override", ] diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index 236161b06..fade711f8 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -281,20 +281,6 @@ class PushNotificationManager { return } - guard let url = constructAPNsURL() else { - let errorMessage = "Failed to construct APNs URL" - LogManager.shared.log(category: .apns, message: errorMessage) - completion(false, errorMessage) - return - } - - guard let jwt = JWTManager.shared.getOrGenerateJWT(keyId: keyId, teamId: teamId, apnsKey: apnsKey) else { - let errorMessage = "Failed to generate JWT, please check that the token is correct." - LogManager.shared.log(category: .apns, message: errorMessage) - completion(false, errorMessage) - return - } - do { guard let messenger = SecureMessenger(sharedSecret: sharedSecret) else { let errorMessage = "Failed to initialize security module. Check shared secret." @@ -306,78 +292,22 @@ class PushNotificationManager { let encryptedDataString = try messenger.encrypt(payload) let finalMessage = EncryptedPushMessage(encryptedData: encryptedDataString, commandType: payload.commandType) - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization") - request.setValue("application/json", forHTTPHeaderField: "content-type") - request.setValue("10", forHTTPHeaderField: "apns-priority") - request.setValue("600", forHTTPHeaderField: "apns-expiration") - request.setValue(bundleId, forHTTPHeaderField: "apns-topic") - request.setValue("alert", forHTTPHeaderField: "apns-push-type") - request.setValue(payload.apnsCollapseID, forHTTPHeaderField: "apns-collapse-id") - - request.httpBody = try JSONEncoder().encode(finalMessage) - - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - let errorMessage = "Failed to send push notification: \(error.localizedDescription)" - LogManager.shared.log(category: .apns, message: errorMessage) - completion(false, errorMessage) - return - } - - if let httpResponse = response as? HTTPURLResponse { - LogManager.shared.log(category: .apns, message: "Push notification sent. Status code: \(httpResponse.statusCode)", isDebug: true) - - var responseBodyMessage = "" - if let data = data, let responseBody = String(data: data, encoding: .utf8) { - LogManager.shared.log(category: .apns, message: "Response body: \(responseBody)", isDebug: true) - if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let reason = json["reason"] as? String - { - responseBodyMessage = reason - } - } - - switch httpResponse.statusCode { - case 200: - completion(true, nil) - case 400: - completion(false, "Bad request. The request was invalid or malformed. \(responseBodyMessage)") - case 403: - JWTManager.shared.invalidateCache() - completion(false, "Authentication error. Check your certificate or authentication token. \(responseBodyMessage)") - case 404: - completion(false, "Invalid request: The :path value was incorrect. \(responseBodyMessage)") - case 405: - completion(false, "Invalid request: Only POST requests are supported. \(responseBodyMessage)") - case 410: - completion(false, "The device token is no longer active for the topic. \(responseBodyMessage)") - case 413: - completion(false, "Payload too large. The notification payload exceeded the size limit. \(responseBodyMessage)") - case 429: - completion(false, "Too many requests. \(responseBodyMessage)") - case 500: - completion(false, "Internal server error at APNs. \(responseBodyMessage)") - case 503: - completion(false, "Service unavailable. The server is temporarily unavailable. Try again later. \(responseBodyMessage)") - default: - completion(false, "Unexpected status code: \(httpResponse.statusCode). \(responseBodyMessage)") - } - } else { - completion(false, "Failed to get a valid HTTP response.") - } - } - task.resume() - + APNSDelivery.send( + body: try JSONEncoder().encode(finalMessage), + deviceToken: deviceToken, + topic: bundleId, + production: productionEnvironment, + keyId: keyId, + teamId: teamId, + apnsKey: apnsKey, + expiration: "600", + collapseID: payload.apnsCollapseID, + completion: completion + ) } catch { let errorMessage = "Failed to encode or encrypt push message: \(error.localizedDescription)" LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) } } - - private func constructAPNsURL() -> URL? { - URL(string: "\(APNSEnvironment.baseURL(production: productionEnvironment))/3/device/\(deviceToken)") - } } diff --git a/LoopFollowWatch/LogManager.swift b/LoopFollowWatch/LogManager.swift new file mode 100644 index 000000000..5494d37f3 --- /dev/null +++ b/LoopFollowWatch/LogManager.swift @@ -0,0 +1,32 @@ +// LoopFollow +// LogManager.swift + +import Foundation +import os + +/// The watch's logger. Same call shape as the phone's LogManager so the code in +/// RemoteShared compiles for both; lines go to the unified system log. +final class LogManager { + static let shared = LogManager() + + enum Category: String { + case apns = "APNS" + case nightscout = "Nightscout" + case dexcom = "Dexcom" + case general = "General" + case remote = "Remote" + } + + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "LoopFollowWatch", category: "watch") + + private init() {} + + func log(category: Category, message: String, isDebug: Bool = false) { + let line = "[\(category.rawValue)] \(LogRedactor.sweep(message))" + if isDebug { + logger.debug("\(line, privacy: .public)") + } else { + logger.log("\(line, privacy: .public)") + } + } +} diff --git a/RemoteShared/APNSDelivery.swift b/RemoteShared/APNSDelivery.swift new file mode 100644 index 000000000..9ec7d44c9 --- /dev/null +++ b/RemoteShared/APNSDelivery.swift @@ -0,0 +1,100 @@ +// LoopFollow +// APNSDelivery.swift + +import Foundation + +/// Sends one remote command push to the Loop or Trio app. Used by the phone and +/// by the watch when it sends without the phone. +enum APNSDelivery { + /// Calls `completion` on an arbitrary queue with success, or the reason for failure. + static func send( + body: Data, + deviceToken: String, + topic: String, + production: Bool, + keyId: String, + teamId: String, + apnsKey: String, + expiration: String? = nil, + collapseID: String? = nil, + completion: @escaping (Bool, String?) -> Void + ) { + guard let jwt = JWTManager.shared.getOrGenerateJWT(keyId: keyId, teamId: teamId, apnsKey: apnsKey) else { + let errorMessage = "Failed to generate JWT, please check that the APNS Key ID, APNS Key and Team ID are correct." + LogManager.shared.log(category: .apns, message: errorMessage) + completion(false, errorMessage) + return + } + + guard let url = URL(string: "\(APNSEnvironment.baseURL(production: production))/3/device/\(deviceToken)") else { + let errorMessage = "Failed to construct APNs URL" + LogManager.shared.log(category: .apns, message: errorMessage) + completion(false, errorMessage) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization") + request.setValue("application/json", forHTTPHeaderField: "content-type") + request.setValue("10", forHTTPHeaderField: "apns-priority") + request.setValue(topic, forHTTPHeaderField: "apns-topic") + request.setValue("alert", forHTTPHeaderField: "apns-push-type") + if let expiration { + request.setValue(expiration, forHTTPHeaderField: "apns-expiration") + } + if let collapseID { + request.setValue(collapseID, forHTTPHeaderField: "apns-collapse-id") + } + request.httpBody = body + + URLSession.shared.dataTask(with: request) { data, response, error in + if let error { + let errorMessage = "Failed to send push notification: \(error.localizedDescription)" + LogManager.shared.log(category: .apns, message: errorMessage) + completion(false, errorMessage) + return + } + guard let httpResponse = response as? HTTPURLResponse else { + let errorMessage = "Failed to get a valid HTTP response." + LogManager.shared.log(category: .apns, message: errorMessage) + completion(false, errorMessage) + return + } + + var reason = "" + if let data, + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let value = json["reason"] as? String + { + reason = value + } + + if httpResponse.statusCode == 200 { + LogManager.shared.log(category: .apns, message: "APNS notification sent successfully") + completion(true, nil) + return + } + if httpResponse.statusCode == 403 { + JWTManager.shared.invalidateCache() + } + LogManager.shared.log(category: .apns, message: "APNS error \(httpResponse.statusCode): \(reason)") + completion(false, "\(message(forStatus: httpResponse.statusCode)) \(reason)") + }.resume() + } + + static func message(forStatus status: Int) -> String { + switch status { + case 400: "Bad request. The request was invalid or malformed." + case 403: "Authentication error. Check your certificate or authentication token." + case 404: "Invalid request: The :path value was incorrect." + case 405: "Invalid request: Only POST requests are supported." + case 410: "The device token is no longer active for the topic." + case 413: "Payload too large. The notification payload exceeded the size limit." + case 429: "Too many requests." + case 500: "Internal server error at APNs." + case 503: "Service unavailable. The server is temporarily unavailable. Try again later." + default: "Unexpected status code: \(status)." + } + } +} diff --git a/LoopFollow/Remote/APNSEnvironment.swift b/RemoteShared/APNSEnvironment.swift similarity index 100% rename from LoopFollow/Remote/APNSEnvironment.swift rename to RemoteShared/APNSEnvironment.swift diff --git a/LoopFollow/Helpers/JWTManager.swift b/RemoteShared/JWTManager.swift similarity index 100% rename from LoopFollow/Helpers/JWTManager.swift rename to RemoteShared/JWTManager.swift diff --git a/LoopFollow/Helpers/LogRedactor.swift b/RemoteShared/LogRedactor.swift similarity index 100% rename from LoopFollow/Helpers/LogRedactor.swift rename to RemoteShared/LogRedactor.swift diff --git a/RemoteShared/LoopAPNSCommands.swift b/RemoteShared/LoopAPNSCommands.swift new file mode 100644 index 000000000..c01c2fd55 --- /dev/null +++ b/RemoteShared/LoopAPNSCommands.swift @@ -0,0 +1,121 @@ +// LoopFollow +// LoopAPNSCommands.swift + +import Foundation + +/// Loop remote command payloads, matching Nightscout's loop.js format. +enum LoopAPNSCommands { + /// Fields every Loop remote command carries. + static func commandHeader(otp: String, alert: String, now: Date) -> [String: Any] { + [ + "otp": otp, + "remote-address": "LoopFollow", + "notes": "Sent via LoopFollow APNS", + "entered-by": "LoopFollow", + "sent-at": formatDateForAPNS(now), + "expiration": formatDateForAPNS(now.addingTimeInterval(5 * 60)), + "alert": alert, + ] + } + + static func carbsCommandPayload(_ payload: LoopAPNSPayload, now: Date = Date()) -> [String: Any] { + let carbsAmount = payload.carbsAmount ?? 0.0 + let absorptionTime = payload.absorptionTime ?? 3.0 + var fields = commandHeader( + otp: payload.otp, + alert: "Remote Carbs Entry: \(String(format: "%.1f", carbsAmount)) grams\nAbsorption Time: \(String(format: "%.1f", absorptionTime)) hours", + now: now + ) + fields["carbs-entry"] = carbsAmount + fields["absorption-time"] = absorptionTime + fields["start-time"] = formatDateForAPNS(payload.consumedDate ?? now) + return fields + } + + static func bolusCommandPayload(_ payload: LoopAPNSPayload, now: Date = Date()) -> [String: Any] { + let bolusAmount = payload.bolusAmount ?? 0.0 + var fields = commandHeader(otp: payload.otp, alert: "Remote Bolus Entry: \(String(format: "%.2f", bolusAmount)) U", now: now) + fields["bolus-entry"] = bolusAmount + return fields + } + + static func carbsDeleteCommandPayload(syncIdentifier: String, otp: String, now: Date = Date()) -> [String: Any] { + var fields = commandHeader(otp: otp, alert: "Remote Carbs Delete", now: now) + fields["carbs-delete"] = syncIdentifier + return fields + } + + /// Keys are prefixed so an unpatched Loop rejects the command. Amount, absorption time and start time are always sent; food type only when set. + static func carbsEditCommandPayload( + syncIdentifier: String, + carbsAmount: Double, + absorptionTimeHours: Double, + foodType: String?, + consumedDate: Date, + otp: String, + now: Date = Date() + ) -> [String: Any] { + var fields = commandHeader( + otp: otp, + alert: "Remote Carbs Edit: \(String(format: "%.1f", carbsAmount)) grams\nAbsorption Time: \(String(format: "%.1f", absorptionTimeHours)) hours", + now: now + ) + fields["carbs-edit"] = syncIdentifier + fields["carbs-edit-entry"] = carbsAmount + fields["carbs-edit-absorption-time"] = absorptionTimeHours + fields["carbs-edit-start-time"] = formatDateForAPNS(consumedDate) + if let foodType, !foodType.isEmpty { + fields["carbs-edit-food-type"] = foodType + } + return fields + } + + /// Creates a properly formatted ISO8601 date string with milliseconds (matching Nightscout's format) + /// - Parameter date: The date to format + /// - Returns: Formatted date string like "2022-12-24T21:34:02.090Z" + /// Wraps command fields in the push payload Loop expects (matching @parse/node-apn). + static func apnsBody(_ payload: [String: Any]) throws -> Data { + var body: [String: Any] = [ + "aps": [ + "alert": payload["alert"] as? String ?? "", + "content-available": 1, + "interruption-level": "time-sensitive", + ], + ] + for (key, value) in payload where key != "alert" && key != "content-available" && key != "interruption-level" { + body[key] = value + } + return try JSONSerialization.data(withJSONObject: body) + } + + static func formatDateForAPNS(_ date: Date) -> String { + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return dateFormatter.string(from: date) + } +} + +struct LoopAPNSPayload { + enum PayloadType { + case carbs + case bolus + } + + let type: PayloadType + let carbsAmount: Double? + let absorptionTime: Double? + let foodType: String? + let bolusAmount: Double? + let consumedDate: Date? + let otp: String + + init(type: PayloadType, carbsAmount: Double? = nil, absorptionTime: Double? = nil, foodType: String? = nil, bolusAmount: Double? = nil, consumedDate: Date? = nil, otp: String) { + self.type = type + self.carbsAmount = carbsAmount + self.absorptionTime = absorptionTime + self.foodType = foodType + self.bolusAmount = bolusAmount + self.consumedDate = consumedDate + self.otp = otp + } +} diff --git a/LoopFollow/Remote/LoopAPNS/OTPSecureMessenger.swift b/RemoteShared/OTPSecureMessenger.swift similarity index 100% rename from LoopFollow/Remote/LoopAPNS/OTPSecureMessenger.swift rename to RemoteShared/OTPSecureMessenger.swift diff --git a/LoopFollow/Remote/TRC/PushMessage.swift b/RemoteShared/PushMessage.swift similarity index 100% rename from LoopFollow/Remote/TRC/PushMessage.swift rename to RemoteShared/PushMessage.swift diff --git a/LoopFollow/Remote/TRC/SecureMessenger.swift b/RemoteShared/SecureMessenger.swift similarity index 100% rename from LoopFollow/Remote/TRC/SecureMessenger.swift rename to RemoteShared/SecureMessenger.swift diff --git a/LoopFollow/Helpers/TOTPGenerator.swift b/RemoteShared/TOTPGenerator.swift similarity index 100% rename from LoopFollow/Helpers/TOTPGenerator.swift rename to RemoteShared/TOTPGenerator.swift diff --git a/LoopFollow/Remote/TRC/TRCCommandType.swift b/RemoteShared/TRCCommandType.swift similarity index 100% rename from LoopFollow/Remote/TRC/TRCCommandType.swift rename to RemoteShared/TRCCommandType.swift diff --git a/Tests/LoopAPNSCommandPayloadTests.swift b/Tests/LoopAPNSCommandPayloadTests.swift index aabd53185..374509bc6 100644 --- a/Tests/LoopAPNSCommandPayloadTests.swift +++ b/Tests/LoopAPNSCommandPayloadTests.swift @@ -12,7 +12,7 @@ struct LoopAPNSCommandPayloadTests { @Test("carbs payload carries the loop.js keys") func carbsKeys() { let payload = LoopAPNSPayload(type: .carbs, carbsAmount: 30, absorptionTime: 3, consumedDate: now.addingTimeInterval(-600), otp: "123456") - let json = LoopAPNSService.carbsCommandPayload(payload, now: now) + let json = LoopAPNSCommands.carbsCommandPayload(payload, now: now) #expect(Set(json.keys) == headerKeys.union(["carbs-entry", "absorption-time", "start-time"])) #expect(json["carbs-entry"] as? Double == 30) #expect(json["absorption-time"] as? Double == 3) @@ -28,7 +28,7 @@ struct LoopAPNSCommandPayloadTests { @Test("bolus payload carries the loop.js keys") func bolusKeys() { - let json = LoopAPNSService.bolusCommandPayload(LoopAPNSPayload(type: .bolus, bolusAmount: 1.25, otp: "123456"), now: now) + let json = LoopAPNSCommands.bolusCommandPayload(LoopAPNSPayload(type: .bolus, bolusAmount: 1.25, otp: "123456"), now: now) #expect(Set(json.keys) == headerKeys.union(["bolus-entry"])) #expect(json["bolus-entry"] as? Double == 1.25) #expect(json["alert"] as? String == "Remote Bolus Entry: 1.25 U") @@ -36,7 +36,7 @@ struct LoopAPNSCommandPayloadTests { @Test("carbs delete addresses the entry by sync identifier") func deleteKeys() { - let json = LoopAPNSService.carbsDeleteCommandPayload(syncIdentifier: "SYNC-1", otp: "123456", now: now) + let json = LoopAPNSCommands.carbsDeleteCommandPayload(syncIdentifier: "SYNC-1", otp: "123456", now: now) #expect(Set(json.keys) == headerKeys.union(["carbs-delete"])) #expect(json["carbs-delete"] as? String == "SYNC-1") #expect(json["carbs-entry"] == nil) @@ -44,7 +44,7 @@ struct LoopAPNSCommandPayloadTests { @Test("carbs edit always sends amount, absorption and start time; food type only when set") func editKeys() { - let json = LoopAPNSService.carbsEditCommandPayload( + let json = LoopAPNSCommands.carbsEditCommandPayload( syncIdentifier: "SYNC-1", carbsAmount: 45, absorptionTimeHours: 2.5, @@ -61,7 +61,7 @@ struct LoopAPNSCommandPayloadTests { #expect(json["carbs-entry"] == nil) #expect(json["start-time"] == nil) - let withFood = LoopAPNSService.carbsEditCommandPayload( + let withFood = LoopAPNSCommands.carbsEditCommandPayload( syncIdentifier: "SYNC-1", carbsAmount: 45, absorptionTimeHours: 2.5, foodType: "🍕", consumedDate: now, otp: "123456", now: now ) #expect(withFood["carbs-edit-food-type"] as? String == "🍕") From e15f77b09a1cf1eeedbaa73c7176b89c387be31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:07:44 +0200 Subject: [PATCH 09/25] Send credentials to the watch when they change, and share command validation between the phone and the watch --- LoopFollow.xcodeproj/project.pbxproj | 4 + .../LiveActivity/GlucoseSnapshotBuilder.swift | 37 +---- .../Remote/LoopAPNS/LoopAPNSService.swift | 50 ++----- .../ReturnNotificationInfo+LoopFollow.swift | 35 +++++ .../Remote/TRC/PushNotificationManager.swift | 70 +++------ LoopFollow/Watch/WatchCommandHandler.swift | 140 +++++------------- .../Watch/WatchCredentialsBuilder.swift | 41 +++++ LoopFollow/Watch/WatchSessionManager.swift | 56 +++++++ LoopFollowWatch/WatchKeychain.swift | 43 ++++++ .../DateExtensions.swift | 0 RemoteShared/OTPSecureMessenger.swift | 2 +- RemoteShared/PushMessage.swift | 18 --- .../ShareClientExtension.swift | 5 +- RemoteShared/WatchCommandValidator.swift | 119 +++++++++++++++ RemoteShared/WatchCredentials.swift | 64 ++++++++ Shared/GlucoseSnapshot.swift | 22 +++ WatchShared/WatchCommand.swift | 24 +++ 17 files changed, 480 insertions(+), 250 deletions(-) create mode 100644 LoopFollow/Remote/ReturnNotificationInfo+LoopFollow.swift create mode 100644 LoopFollow/Watch/WatchCredentialsBuilder.swift create mode 100644 LoopFollowWatch/WatchKeychain.swift rename {LoopFollow/Helpers => RemoteShared}/DateExtensions.swift (100%) rename {LoopFollow/Extensions => RemoteShared}/ShareClientExtension.swift (96%) create mode 100644 RemoteShared/WatchCommandValidator.swift create mode 100644 RemoteShared/WatchCredentials.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 9397123bb..e9db02c3f 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; }; DD57A7C02F70000000000001 /* LoopFollowWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = DD57A7C02F70000000000002 /* LoopFollowWatch.app */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DD57A7C02F70000000000003 /* LoopFollowWatchWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DD57A7C02F70000000000004 /* LoopFollowWatchWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DD57A7C02F70000000000103 /* ShareClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD57A7C02F70000000000102 /* ShareClient.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -108,6 +109,7 @@ FCFEEC9D2486E68E00402A7F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; DD57A7C02F70000000000002 /* LoopFollowWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LoopFollowWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; DD57A7C02F70000000000004 /* LoopFollowWatchWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LoopFollowWatchWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DD57A7C02F70000000000102 /* ShareClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ShareClient.swift; path = Pods/ShareClient/ShareClient/ShareClient.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -290,6 +292,7 @@ children = ( ECA3EFB4037410B4973BB632 /* Pods-LoopFollow.debug.xcconfig */, 059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */, + DD57A7C02F70000000000102 /* ShareClient.swift */, ); path = Pods; sourceTree = ""; @@ -667,6 +670,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DD57A7C02F70000000000103 /* ShareClient.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift b/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift index 3010a98c3..bb59255dd 100644 --- a/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift +++ b/LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift @@ -132,7 +132,7 @@ enum GlucoseSnapshotBuilder { let preferredUnit = PreferredGlucoseUnit.snapshotUnit() let deltaMgdl = provider.deltaMgdl ?? 0.0 - let trend = mapTrend(provider.trendCode) + let trend = GlucoseSnapshot.Trend(code: provider.trendCode) if provider.showRenewalOverlay { LogManager.shared.log(category: .general, message: "[LA] renewal overlay ON") @@ -180,39 +180,4 @@ enum GlucoseSnapshotBuilder { showRenewalOverlay: provider.showRenewalOverlay, ) } - - // MARK: - Trend Mapping - - private static func mapTrend(_ code: String?) -> GlucoseSnapshot.Trend { - guard - let raw = code? - .trimmingCharacters(in: .whitespacesAndNewlines) - .lowercased(), - !raw.isEmpty - else { return .unknown } - - if raw.contains("doubleup") || raw.contains("rapidrise") || raw == "up2" || raw == "upfast" { - return .upFast - } - if raw.contains("fortyfiveup") { - return .upSlight - } - if raw.contains("singleup") || raw == "up" || raw == "up1" || raw == "rising" { - return .up - } - if raw.contains("flat") || raw == "steady" || raw == "none" { - return .flat - } - if raw.contains("doubledown") || raw.contains("rapidfall") || raw == "down2" || raw == "downfast" { - return .downFast - } - if raw.contains("fortyfivedown") { - return .downSlight - } - if raw.contains("singledown") || raw == "down" || raw == "down1" || raw == "falling" { - return .down - } - - return .unknown - } } diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift index 658986db1..e9876fc3b 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift @@ -10,15 +10,13 @@ class LoopAPNSService { /// Returns the effective APNs credentials for sending commands to the remote app. /// Same team → use LoopFollow's own key. Different team → use remote-specific key. private func effectiveCredentials() -> (apnsKey: String, keyId: String, teamId: String) { - let lfTeamId = BuildDetails.default.teamID ?? "" - let remoteTeamId = storage.teamId.value ?? "" - let sameTeam = !lfTeamId.isEmpty && !remoteTeamId.isEmpty && lfTeamId == remoteTeamId - - if sameTeam || remoteTeamId.isEmpty { - return (storage.lfApnsKey.value, storage.lfKeyId.value, lfTeamId) - } else { - return (storage.remoteApnsKey.value, storage.remoteKeyId.value, remoteTeamId) - } + let resolved = RemoteSigningKey.resolve( + lfTeamID: BuildDetails.default.teamID ?? "", + remoteTeamID: storage.teamId.value ?? "", + lfKey: (storage.lfApnsKey.value, storage.lfKeyId.value), + remoteKey: (storage.remoteApnsKey.value, storage.remoteKeyId.value) + ) + return (resolved.key, resolved.keyID, resolved.teamID) } enum LoopAPNSError: Error, LocalizedError { @@ -59,34 +57,6 @@ class LoopAPNSService { } } - private func createReturnNotificationInfo() -> ReturnNotificationInfo? { - let loopFollowDeviceToken = Observable.shared.loopFollowDeviceToken.value - guard !loopFollowDeviceToken.isEmpty else { return nil } - - // Get LoopFollow's own Team ID from BuildDetails. - guard let loopFollowTeamID = BuildDetails.default.teamID, !loopFollowTeamID.isEmpty else { - LogManager.shared.log(category: .apns, message: "LoopFollow Team ID not found in BuildDetails.plist. Cannot create return notification info.") - return nil - } - - let lfKeyId = storage.lfKeyId.value - let lfApnsKey = storage.lfApnsKey.value - - guard !lfKeyId.isEmpty, !lfApnsKey.isEmpty else { - LogManager.shared.log(category: .apns, message: "Missing LoopFollow APNS credentials. Configure them in App Settings → APN.") - return nil - } - - return ReturnNotificationInfo( - productionEnvironment: BuildDetails.default.isTestFlightBuild(), - deviceToken: loopFollowDeviceToken, - bundleId: Bundle.main.bundleIdentifier ?? "", - teamId: loopFollowTeamID, - keyId: lfKeyId, - apnsKey: lfApnsKey - ) - } - /// Encrypts return notification info using OTP code private func encryptReturnNotificationInfo(returnInfo: ReturnNotificationInfo, otpCode: String) -> String? { guard let messenger = OTPSecureMessenger(otpCode: otpCode) else { @@ -126,7 +96,7 @@ class LoopAPNSService { /// The return-notification block encrypted with the OTP, when LoopFollow's own credentials are configured. private func encryptedReturnNotification(otp: String) -> String? { - guard let returnInfo = createReturnNotificationInfo() else { + guard let returnInfo = ReturnNotificationInfo.loopFollow() else { LogManager.shared.log(category: .apns, message: "Return notification info unavailable; Loop cannot confirm this command") return nil } @@ -523,7 +493,7 @@ class LoopAPNSService { // For override commands, we can include return notification info unencrypted // since override commands don't require OTP validation in Loop - if let returnInfo = createReturnNotificationInfo() { + if let returnInfo = ReturnNotificationInfo.loopFollow() { payload["return_notification"] = [ "production_environment": returnInfo.productionEnvironment, "device_token": returnInfo.deviceToken, @@ -579,7 +549,7 @@ class LoopAPNSService { // For override commands, we can include return notification info unencrypted // since override commands don't require OTP validation in Loop - if let returnInfo = createReturnNotificationInfo() { + if let returnInfo = ReturnNotificationInfo.loopFollow() { payload["return_notification"] = [ "production_environment": returnInfo.productionEnvironment, "device_token": returnInfo.deviceToken, diff --git a/LoopFollow/Remote/ReturnNotificationInfo+LoopFollow.swift b/LoopFollow/Remote/ReturnNotificationInfo+LoopFollow.swift new file mode 100644 index 000000000..34c61d15a --- /dev/null +++ b/LoopFollow/Remote/ReturnNotificationInfo+LoopFollow.swift @@ -0,0 +1,35 @@ +// LoopFollow +// ReturnNotificationInfo+LoopFollow.swift + +import Foundation + +extension ReturnNotificationInfo { + /// How Loop or Trio reaches this LoopFollow app to confirm a command, or nil + /// when LoopFollow's own APNs credentials or device token are missing. + static func loopFollow() -> ReturnNotificationInfo? { + let loopFollowDeviceToken = Observable.shared.loopFollowDeviceToken.value + guard !loopFollowDeviceToken.isEmpty else { return nil } + + guard let loopFollowTeamID = BuildDetails.default.teamID, !loopFollowTeamID.isEmpty else { + LogManager.shared.log(category: .apns, message: "LoopFollow Team ID not found in BuildDetails.plist. Cannot create return notification info.") + return nil + } + + let lfKeyId = Storage.shared.lfKeyId.value + let lfApnsKey = Storage.shared.lfApnsKey.value + + guard !lfKeyId.isEmpty, !lfApnsKey.isEmpty else { + LogManager.shared.log(category: .apns, message: "Missing LoopFollow APNS credentials. Configure them in App Settings → APN.") + return nil + } + + return ReturnNotificationInfo( + productionEnvironment: BuildDetails.default.isTestFlightBuild(), + deviceToken: loopFollowDeviceToken, + bundleId: Bundle.main.bundleIdentifier ?? "", + teamId: loopFollowTeamID, + keyId: lfKeyId, + apnsKey: lfApnsKey + ) + } +} diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index fade711f8..7a6513a15 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -21,49 +21,15 @@ class PushNotificationManager { user = Storage.shared.user.value bundleId = Storage.shared.bundleId.value - let lfTeamId = BuildDetails.default.teamID ?? "" - let remoteTeamId = Storage.shared.teamId.value ?? "" - let sameTeam = !lfTeamId.isEmpty && !remoteTeamId.isEmpty && lfTeamId == remoteTeamId - - if sameTeam || remoteTeamId.isEmpty { - apnsKey = Storage.shared.lfApnsKey.value - keyId = Storage.shared.lfKeyId.value - teamId = lfTeamId - } else { - apnsKey = Storage.shared.remoteApnsKey.value - keyId = Storage.shared.remoteKeyId.value - teamId = remoteTeamId - } - } - - private func createReturnNotificationInfo() -> CommandPayload.ReturnNotificationInfo? { - let loopFollowDeviceToken = Observable.shared.loopFollowDeviceToken.value - - guard !loopFollowDeviceToken.isEmpty else { - return nil - } - - guard let loopFollowTeamID = BuildDetails.default.teamID, !loopFollowTeamID.isEmpty else { - LogManager.shared.log(category: .apns, message: "LoopFollow Team ID not found in BuildDetails.plist. Cannot create return notification info.") - return nil - } - - let lfKeyId = Storage.shared.lfKeyId.value - let lfApnsKey = Storage.shared.lfApnsKey.value - - guard !lfKeyId.isEmpty, !lfApnsKey.isEmpty else { - LogManager.shared.log(category: .apns, message: "Missing LoopFollow APNS credentials. Configure them in App Settings → APN.") - return nil - } - - return CommandPayload.ReturnNotificationInfo( - productionEnvironment: BuildDetails.default.isTestFlightBuild(), - deviceToken: loopFollowDeviceToken, - bundleId: Bundle.main.bundleIdentifier ?? "", - teamId: loopFollowTeamID, - keyId: lfKeyId, - apnsKey: lfApnsKey + let resolved = RemoteSigningKey.resolve( + lfTeamID: BuildDetails.default.teamID ?? "", + remoteTeamID: Storage.shared.teamId.value ?? "", + lfKey: (Storage.shared.lfApnsKey.value, Storage.shared.lfKeyId.value), + remoteKey: (Storage.shared.remoteApnsKey.value, Storage.shared.remoteKeyId.value) ) + apnsKey = resolved.key + keyId = resolved.keyID + teamId = resolved.teamID } func sendOverridePushNotification(override: ProfileManager.TrioOverride, completion: @escaping (Bool, String?) -> Void) { @@ -72,7 +38,7 @@ class PushNotificationManager { commandType: .startOverride, timestamp: Date().timeIntervalSince1970, overrideName: override.name, - returnNotification: createReturnNotificationInfo() + returnNotification: ReturnNotificationInfo.loopFollow() ) sendEncryptedCommand(payload: payload, completion: completion) } @@ -83,7 +49,7 @@ class PushNotificationManager { commandType: .cancelOverride, timestamp: Date().timeIntervalSince1970, overrideName: nil, - returnNotification: createReturnNotificationInfo() + returnNotification: ReturnNotificationInfo.loopFollow() ) sendEncryptedCommand(payload: payload, completion: completion) } @@ -95,7 +61,7 @@ class PushNotificationManager { commandType: .bolus, timestamp: Date().timeIntervalSince1970, bolusAmount: bolusAmountDecimal, - returnNotification: createReturnNotificationInfo() + returnNotification: ReturnNotificationInfo.loopFollow() ) sendEncryptedCommand(payload: payload, completion: completion) } @@ -109,7 +75,7 @@ class PushNotificationManager { timestamp: Date().timeIntervalSince1970, target: targetValue, duration: durationValue, - returnNotification: createReturnNotificationInfo() + returnNotification: ReturnNotificationInfo.loopFollow() ) sendEncryptedCommand(payload: payload, completion: completion) } @@ -119,7 +85,7 @@ class PushNotificationManager { user: user, commandType: .cancelTempTarget, timestamp: Date().timeIntervalSince1970, - returnNotification: createReturnNotificationInfo() + returnNotification: ReturnNotificationInfo.loopFollow() ) sendEncryptedCommand(payload: payload, completion: completion) } @@ -159,14 +125,14 @@ class PushNotificationManager { protein: proteinValue, fat: fatValue, scheduledTime: scheduledTimeInterval, - returnNotification: createReturnNotificationInfo() + returnNotification: ReturnNotificationInfo.loopFollow() ) sendEncryptedCommand(payload: payload, completion: completion) } /// Trio confirms edit/delete through a return notification, so LoopFollow's own APNs credentials are required. func sendDeleteMealPushNotification(mealID: String, commandID: String, completion: @escaping (Bool, String?) -> Void) { - guard let returnNotification = createReturnNotificationInfo() else { + guard let returnNotification = ReturnNotificationInfo.loopFollow() else { completion(false, Self.returnNotificationRequiredMessage) return } @@ -194,7 +160,7 @@ class PushNotificationManager { completion(false, "No nutrient data provided. At least one of carbs, fat, or protein must be greater than 0.") return } - guard let returnNotification = createReturnNotificationInfo() else { + guard let returnNotification = ReturnNotificationInfo.loopFollow() else { completion(false, Self.returnNotificationRequiredMessage) return } @@ -292,8 +258,8 @@ class PushNotificationManager { let encryptedDataString = try messenger.encrypt(payload) let finalMessage = EncryptedPushMessage(encryptedData: encryptedDataString, commandType: payload.commandType) - APNSDelivery.send( - body: try JSONEncoder().encode(finalMessage), + try APNSDelivery.send( + body: JSONEncoder().encode(finalMessage), deviceToken: deviceToken, topic: bundleId, production: productionEnvironment, diff --git a/LoopFollow/Watch/WatchCommandHandler.swift b/LoopFollow/Watch/WatchCommandHandler.swift index c861677a5..238e1d964 100644 --- a/LoopFollow/Watch/WatchCommandHandler.swift +++ b/LoopFollow/Watch/WatchCommandHandler.swift @@ -9,19 +9,26 @@ import HealthKit final class WatchCommandHandler { static let shared = WatchCommandHandler() - /// Loop rejects carb entries outside this window; same limits as LoopAPNSCarbsView. - private let loopMaxPastHours: Double = 12 - private let loopMaxFutureHours: Double = 1 - /// Loop's default absorption time for a remote carb entry, in hours. - private let loopAbsorptionHours = 3.0 - private var handledIDs: [UUID] = [] private init() {} + /// The guardrails from the phone's Remote settings. + static func limits() -> WatchCommandValidator.Limits { + WatchCommandValidator.Limits( + maxBolus: Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()), + bolusIncrement: Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit()), + maxCarbs: Storage.shared.maxCarbs.value.doubleValue(for: .gram()), + maxFat: Storage.shared.maxFat.value.doubleValue(for: .gram()), + maxProtein: Storage.shared.maxProtein.value.doubleValue(for: .gram()), + mealWithBolus: Storage.shared.mealWithBolus.value, + mealWithFatProtein: Storage.shared.mealWithFatProtein.value + ) + } + func handle(_ command: WatchCommand, reply: @escaping (WatchCommandReply) -> Void) { let finish: (Bool, String) -> Void = { success, message in - LogManager.shared.log(category: .apns, message: "Watch command \(Self.name(of: command.action)) finished: success=\(success) \(message)") + LogManager.shared.log(category: .apns, message: "Watch command \(command.action.name) finished: success=\(success) \(message)") reply(WatchCommandReply(success: success, message: message)) } @@ -37,7 +44,7 @@ final class WatchCommandHandler { return } - LogManager.shared.log(category: .apns, message: "Watch command received: \(Self.name(of: command.action))") + LogManager.shared.log(category: .apns, message: "Watch command received: \(command.action.name)") if case .snoozeAlarm = command.action { guard Observable.shared.currentAlarm.value != nil else { @@ -53,20 +60,25 @@ final class WatchCommandHandler { finish(false, "Commands from Apple Watch are turned off in LoopFollow's Remote settings.") return } - - switch WatchStateBuilder.remoteKind() { - case .loop: - handleLoop(command.action, finish: finish) - case .trio: - handleTrio(command.action, finish: finish) - case nil: + guard let kind = WatchStateBuilder.remoteKind() else { finish(false, "Remote control is not set up in LoopFollow.") + return + } + + switch WatchCommandValidator.validate(command.action, kind: kind, limits: Self.limits()) { + case let .failure(error): + finish(false, error.message) + case let .success(action): + switch kind { + case .loop: sendLoop(action, finish: finish) + case .trio: sendTrio(action, finish: finish) + } } } // MARK: - Loop - private func handleLoop(_ action: WatchCommand.Action, finish: @escaping (Bool, String) -> Void) { + private func sendLoop(_ action: WatchCommand.Action, finish: @escaping (Bool, String) -> Void) { let service = LoopAPNSService() let sent: (Bool, String?) -> Void = { success, error in DispatchQueue.main.async { @@ -76,8 +88,6 @@ final class WatchCommandHandler { switch action { case let .bolus(units): - guard units > 0 else { return finish(false, "Enter an amount above zero.") } - guard units <= maxBolus else { return finish(false, exceedsMaxBolus(units)) } guard service.validateSetup() else { return finish(false, loopSetupIncomplete) } guard let otp = loopOTP(finish: finish) else { return } service.sendBolusViaAPNS(payload: LoopAPNSPayload(type: .bolus, bolusAmount: units, otp: otp)) { success, error in @@ -90,23 +100,13 @@ final class WatchCommandHandler { sent(success, error) } - case let .meal(carbs, fat, protein, bolus, consumedAt): - guard fat == 0, protein == 0, bolus == 0 else { return finish(false, "Loop accepts carbs only from the watch.") } - guard carbs > 0 else { return finish(false, "Enter an amount above zero.") } - guard carbs <= maxCarbs else { return finish(false, exceeds("Carbs", carbs, maxCarbs)) } - let now = Date() - guard consumedAt >= now.addingTimeInterval(-loopMaxPastHours * 3600) else { - return finish(false, "Time must be within the prior \(Int(loopMaxPastHours)) hours.") - } - guard consumedAt <= now.addingTimeInterval(loopMaxFutureHours * 3600) else { - return finish(false, "Time must be within the next \(Int(loopMaxFutureHours)) hour.") - } + case let .meal(carbs, _, _, _, consumedAt): guard service.validateSetup() else { return finish(false, loopSetupIncomplete) } guard let otp = loopOTP(finish: finish) else { return } let payload = LoopAPNSPayload( type: .carbs, carbsAmount: carbs, - absorptionTime: loopAbsorptionHours, + absorptionTime: WatchCommandValidator.loopAbsorptionHours, consumedDate: consumedAt.dateUsingCurrentSeconds(), otp: otp ) @@ -130,11 +130,8 @@ final class WatchCommandHandler { case .cancelOverride: service.sendCancelOverrideNotification(completion: sent) - case .startTempTarget, .cancelTempTarget: - finish(false, "Temp targets are not available for Loop.") - - case .snoozeAlarm: - break + case .startTempTarget, .cancelTempTarget, .snoozeAlarm: + finish(false, "This command is not available for Loop.") } } @@ -158,7 +155,7 @@ final class WatchCommandHandler { // MARK: - Trio - private func handleTrio(_ action: WatchCommand.Action, finish: @escaping (Bool, String) -> Void) { + private func sendTrio(_ action: WatchCommand.Action, finish: @escaping (Bool, String) -> Void) { let manager = PushNotificationManager() let sent: (Bool, String?) -> Void = { success, error in DispatchQueue.main.async { @@ -168,46 +165,24 @@ final class WatchCommandHandler { switch action { case let .bolus(units): - let stepped = roundedToBolusStep(units) - guard stepped > 0 else { return finish(false, "Enter an amount above zero.") } - guard stepped <= maxBolus else { return finish(false, exceedsMaxBolus(stepped)) } - manager.sendBolusPushNotification(bolusAmount: HKQuantity(unit: .internationalUnit(), doubleValue: stepped)) { success, error in + manager.sendBolusPushNotification(bolusAmount: HKQuantity(unit: .internationalUnit(), doubleValue: units)) { success, error in if success { - DispatchQueue.main.async { QuickPickBolusesManager.shared.recordBolus(units: stepped) } + DispatchQueue.main.async { QuickPickBolusesManager.shared.recordBolus(units: units) } } sent(success, error) } case let .meal(carbs, fat, protein, bolus, consumedAt): - let withFatProtein = Storage.shared.mealWithFatProtein.value - let withBolus = Storage.shared.mealWithBolus.value - guard withFatProtein || (fat == 0 && protein == 0) else { return finish(false, "Meal with Fat/Protein is turned off in LoopFollow's Remote settings.") } - guard withBolus || bolus == 0 else { return finish(false, "Meal with Bolus is turned off in LoopFollow's Remote settings.") } - guard carbs >= 0, fat >= 0, protein >= 0, carbs > 0 || fat > 0 || protein > 0 else { - return finish(false, "Enter carbs, fat or protein.") - } - guard carbs <= maxCarbs else { return finish(false, exceeds("Carbs", carbs, maxCarbs)) } - guard fat <= Storage.shared.maxFat.value.doubleValue(for: .gram()) else { - return finish(false, exceeds("Fat", fat, Storage.shared.maxFat.value.doubleValue(for: .gram()))) - } - guard protein <= Storage.shared.maxProtein.value.doubleValue(for: .gram()) else { - return finish(false, exceeds("Protein", protein, Storage.shared.maxProtein.value.doubleValue(for: .gram()))) - } - let steppedBolus = roundedToBolusStep(bolus) - guard steppedBolus <= maxBolus else { return finish(false, exceedsMaxBolus(steppedBolus)) } - - // Within a minute of now counts as "now", as in the phone's meal screen. - let scheduledTime: Date? = abs(consumedAt.timeIntervalSinceNow) < 60 ? nil : consumedAt.dateUsingCurrentSeconds() manager.sendMealPushNotification( carbs: HKQuantity(unit: .gram(), doubleValue: carbs), protein: HKQuantity(unit: .gram(), doubleValue: protein), fat: HKQuantity(unit: .gram(), doubleValue: fat), - bolusAmount: HKQuantity(unit: .internationalUnit(), doubleValue: steppedBolus), - scheduledTime: scheduledTime + bolusAmount: HKQuantity(unit: .internationalUnit(), doubleValue: bolus), + scheduledTime: WatchCommand.trioScheduledTime(consumedAt) ) { success, error in if success, carbs > 0 { DispatchQueue.main.async { - QuickPickMealsManager.shared.recordMeal(carbs: carbs, fat: fat, protein: protein, bolus: steppedBolus) + QuickPickMealsManager.shared.recordMeal(carbs: carbs, fat: fat, protein: protein, bolus: bolus) } } sent(success, error) @@ -232,44 +207,7 @@ final class WatchCommandHandler { manager.sendCancelTempTargetPushNotification(completion: sent) case .snoozeAlarm: - break - } - } - - // MARK: - Helpers - - private var maxBolus: Double { - Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()) - } - - private var maxCarbs: Double { - Storage.shared.maxCarbs.value.doubleValue(for: .gram()) - } - - /// Rounds down to the bolus increment, as the phone's Trio bolus screen does. - private func roundedToBolusStep(_ value: Double) -> Double { - let step = max(0.001, Storage.shared.bolusIncrement.value.doubleValue(for: .internationalUnit())) - let stepped = ((value / step) + 1e-10).rounded(.down) * step - return (stepped * 1000).rounded() / 1000 - } - - private func exceedsMaxBolus(_ units: Double) -> String { - "\(InsulinFormatter.shared.string(units)) U exceeds the maximum bolus of \(InsulinFormatter.shared.string(maxBolus)) U set in LoopFollow." - } - - private func exceeds(_ what: String, _ grams: Double, _ max: Double) -> String { - "\(what) (\(Int(grams)) g) exceeds the maximum of \(Int(max)) g set in LoopFollow." - } - - private static func name(of action: WatchCommand.Action) -> String { - switch action { - case .bolus: "bolus" - case .meal: "meal" - case .startOverride: "startOverride" - case .cancelOverride: "cancelOverride" - case .startTempTarget: "startTempTarget" - case .cancelTempTarget: "cancelTempTarget" - case .snoozeAlarm: "snoozeAlarm" + finish(false, "Unknown request.") } } } diff --git a/LoopFollow/Watch/WatchCredentialsBuilder.swift b/LoopFollow/Watch/WatchCredentialsBuilder.swift new file mode 100644 index 000000000..52ef3965d --- /dev/null +++ b/LoopFollow/Watch/WatchCredentialsBuilder.swift @@ -0,0 +1,41 @@ +// LoopFollow +// WatchCredentialsBuilder.swift + +import Foundation + +/// Collects what the watch needs to fetch data and send commands on its own. +enum WatchCredentialsBuilder { + static func build() -> WatchCredentials { + WatchCredentials( + nightscoutURL: Storage.shared.url.value, + nightscoutToken: Storage.shared.token.value, + dexcomUsername: Storage.shared.shareUserName.value, + dexcomPassword: Storage.shared.sharePassword.value, + dexcomServer: Storage.shared.shareServer.value, + remote: remote() + ) + } + + private static func remote() -> WatchCredentials.Remote? { + guard Storage.shared.watchRemoteCommands.value, let kind = WatchStateBuilder.remoteKind() else { return nil } + let signing = RemoteSigningKey.resolve( + lfTeamID: BuildDetails.default.teamID ?? "", + remoteTeamID: Storage.shared.teamId.value ?? "", + lfKey: (Storage.shared.lfApnsKey.value, Storage.shared.lfKeyId.value), + remoteKey: (Storage.shared.remoteApnsKey.value, Storage.shared.remoteKeyId.value) + ) + return WatchCredentials.Remote( + kind: kind, + deviceToken: Storage.shared.deviceToken.value, + bundleID: Storage.shared.bundleId.value, + production: Storage.shared.productionEnvironment.value, + apnsKey: signing.key, + keyID: signing.keyID, + teamID: signing.teamID, + user: Storage.shared.user.value, + sharedSecret: Storage.shared.sharedSecret.value, + loopQRCodeURL: Storage.shared.loopAPNSQrCodeURL.value, + returnNotification: ReturnNotificationInfo.loopFollow() + ) + } +} diff --git a/LoopFollow/Watch/WatchSessionManager.swift b/LoopFollow/Watch/WatchSessionManager.swift index 70619ff99..8f8b14b38 100644 --- a/LoopFollow/Watch/WatchSessionManager.swift +++ b/LoopFollow/Watch/WatchSessionManager.swift @@ -16,6 +16,7 @@ final class WatchSessionManager: NSObject { private let complicationTransferInterval: TimeInterval = 15 * 60 private var lastSentState: WatchState? + private var lastSentCredentials: WatchCredentials? private var lastComplicationTransfer: Date = .distantPast private var cancellables = Set() private lazy var coalescer = RefreshCoalescer(delay: 2) { [weak self] reason in @@ -59,6 +60,48 @@ final class WatchSessionManager: NSObject { .dropFirst(triggers.count) .sink { [weak self] in self?.requestUpdate(reason: "settings") } .store(in: &cancellables) + + let credentialTriggers: [AnyPublisher] = [ + Storage.shared.url.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.token.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.shareUserName.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.sharePassword.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.shareServer.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.remoteType.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.watchRemoteCommands.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.deviceToken.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.bundleId.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.productionEnvironment.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.teamId.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.lfApnsKey.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.lfKeyId.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.remoteApnsKey.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.remoteKeyId.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.user.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.sharedSecret.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.loopAPNSQrCodeURL.$value.map { _ in () }.eraseToAnyPublisher(), + Observable.shared.loopFollowDeviceToken.$value.map { _ in () }.eraseToAnyPublisher(), + ] + Publishers.MergeMany(credentialTriggers) + .debounce(for: .seconds(2), scheduler: DispatchQueue.main) + .sink { [weak self] in self?.sendCredentialsIfChanged(force: false) } + .store(in: &cancellables) + } + + /// Sends the credentials when they differ from what was last sent, or when + /// the watch asks for them. They never travel with the state updates. + private func sendCredentialsIfChanged(force: Bool) { + guard let session, session.activationState == .activated, session.isPaired, session.isWatchAppInstalled else { return } + let credentials = WatchCredentialsBuilder.build() + guard force || credentials != lastSentCredentials, + let data = try? JSONEncoder().encode(credentials) + else { return } + for transfer in session.outstandingUserInfoTransfers where transfer.userInfo[WatchCredentials.userInfoKey] != nil { + transfer.cancel() + } + session.transferUserInfo([WatchCredentials.userInfoKey: data]) + lastSentCredentials = credentials + LogManager.shared.log(category: .general, message: "[Watch] credentials sent") } /// Sends the info table to the watch whenever one of its rows changes. @@ -104,6 +147,7 @@ extension WatchSessionManager: WCSessionDelegate { } if state == .activated { requestUpdate(reason: "activated") + DispatchQueue.main.async { self.sendCredentialsIfChanged(force: false) } } } @@ -120,6 +164,12 @@ extension WatchSessionManager: WCSessionDelegate { func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { DispatchQueue.main.async { + if message[WatchCredentials.requestKey] != nil { + self.sendCredentialsIfChanged(force: true) + replyHandler([:]) + return + } + if message[WatchMessageKey.requestState] != nil { let data = WatchStateBuilder.build().encoded() ?? Data() replyHandler([WatchState.contextKey: data]) @@ -140,6 +190,12 @@ extension WatchSessionManager: WCSessionDelegate { } } + func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { + if userInfo[WatchCredentials.requestKey] != nil { + DispatchQueue.main.async { self.sendCredentialsIfChanged(force: true) } + } + } + private static func reply(_ reply: WatchCommandReply, to handler: ([String: Any]) -> Void) { let data = (try? JSONEncoder().encode(reply)) ?? Data() handler([WatchCommandReply.messageKey: data]) diff --git a/LoopFollowWatch/WatchKeychain.swift b/LoopFollowWatch/WatchKeychain.swift new file mode 100644 index 000000000..f5db58dbc --- /dev/null +++ b/LoopFollowWatch/WatchKeychain.swift @@ -0,0 +1,43 @@ +// LoopFollow +// WatchKeychain.swift + +import Foundation +import Security + +/// Stores the credentials from the phone in the watch's Keychain. Items are +/// readable after the first unlock and never leave this watch, not even in a backup. +enum WatchKeychain { + private static let service = "LoopFollowWatch" + private static let account = "credentials" + + private static var query: [String: Any] { + [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + ] + } + + static func save(_ credentials: WatchCredentials) { + guard let data = try? JSONEncoder().encode(credentials) else { return } + let attributes: [String: Any] = [ + kSecValueData as String: data, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + ] + let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) + if status == errSecItemNotFound { + SecItemAdd(query.merging(attributes) { $1 } as CFDictionary, nil) + } + } + + static func load() -> WatchCredentials? { + var search = query + search[kSecReturnData as String] = true + search[kSecMatchLimit as String] = kSecMatchLimitOne + var result: AnyObject? + guard SecItemCopyMatching(search as CFDictionary, &result) == errSecSuccess, + let data = result as? Data + else { return nil } + return try? JSONDecoder().decode(WatchCredentials.self, from: data) + } +} diff --git a/LoopFollow/Helpers/DateExtensions.swift b/RemoteShared/DateExtensions.swift similarity index 100% rename from LoopFollow/Helpers/DateExtensions.swift rename to RemoteShared/DateExtensions.swift diff --git a/RemoteShared/OTPSecureMessenger.swift b/RemoteShared/OTPSecureMessenger.swift index 02762338b..4e201a965 100644 --- a/RemoteShared/OTPSecureMessenger.swift +++ b/RemoteShared/OTPSecureMessenger.swift @@ -38,7 +38,7 @@ struct OTPSecureMessenger { } /// Information needed to send a response notification back via APNS -struct ReturnNotificationInfo: Codable { +struct ReturnNotificationInfo: Codable, Equatable { let productionEnvironment: Bool let deviceToken: String let bundleId: String diff --git a/RemoteShared/PushMessage.swift b/RemoteShared/PushMessage.swift index 319482c7c..1d6267ae6 100644 --- a/RemoteShared/PushMessage.swift +++ b/RemoteShared/PushMessage.swift @@ -54,24 +54,6 @@ struct CommandPayload: Encodable { return commandType.rawValue } - struct ReturnNotificationInfo: Encodable { - let productionEnvironment: Bool - let deviceToken: String - let bundleId: String - let teamId: String - let keyId: String - let apnsKey: String - - enum CodingKeys: String, CodingKey { - case productionEnvironment = "production_environment" - case deviceToken = "device_token" - case bundleId = "bundle_id" - case teamId = "team_id" - case keyId = "key_id" - case apnsKey = "apns_key" - } - } - enum CodingKeys: String, CodingKey { case user case commandType = "command_type" diff --git a/LoopFollow/Extensions/ShareClientExtension.swift b/RemoteShared/ShareClientExtension.swift similarity index 96% rename from LoopFollow/Extensions/ShareClientExtension.swift rename to RemoteShared/ShareClientExtension.swift index 6201e9100..c8b492b78 100644 --- a/LoopFollow/Extensions/ShareClientExtension.swift +++ b/RemoteShared/ShareClientExtension.swift @@ -2,7 +2,9 @@ // ShareClientExtension.swift import Foundation -import ShareClient +#if canImport(ShareClient) + import ShareClient +#endif public struct ShareGlucoseData: Decodable { var sgv: Int @@ -56,7 +58,6 @@ private var TrendTable: [String] = [ "RATE OUT OF RANGE", // 9 ] -// TODO: probably better to make this an inherited class rather than an extension public extension ShareClient { func fetchData(_ entries: Int, callback: @escaping (ShareError?, [ShareGlucoseData]?) -> Void) { fetchLast(entries) { error, result in diff --git a/RemoteShared/WatchCommandValidator.swift b/RemoteShared/WatchCommandValidator.swift new file mode 100644 index 000000000..3bd475a81 --- /dev/null +++ b/RemoteShared/WatchCommandValidator.swift @@ -0,0 +1,119 @@ +// LoopFollow +// WatchCommandValidator.swift + +import Foundation + +/// The guardrails for a watch command, applied by the phone before it sends +/// one and by the watch before it sends one directly. Same limits and messages +/// as the phone's Remote screens. +enum WatchCommandValidator { + struct Limits: Equatable { + var maxBolus: Double + var bolusIncrement: Double + var maxCarbs: Double + var maxFat: Double + var maxProtein: Double + var mealWithBolus: Bool + var mealWithFatProtein: Bool + } + + /// Loop rejects carb entries outside this window; same limits as LoopAPNSCarbsView. + static let loopMaxPastHours: Double = 12 + static let loopMaxFutureHours: Double = 1 + /// Loop's default absorption time for a remote carb entry, in hours. + static let loopAbsorptionHours = 3.0 + + /// Returns the action to send (Trio boluses rounded down to the increment) or the reason it is refused. + static func validate( + _ action: WatchCommand.Action, + kind: WatchRemoteConfig.Kind, + limits: Limits, + now: Date = Date() + ) -> Result { + switch (kind, action) { + case let (.loop, .bolus(units)): + guard units > 0 else { return .failure(ValidationError("Enter an amount above zero.")) } + guard units <= limits.maxBolus else { return .failure(ValidationError(exceedsMaxBolus(units, limits))) } + return .success(action) + + case let (.trio, .bolus(units)): + let stepped = roundedToStep(units, step: limits.bolusIncrement) + guard stepped > 0 else { return .failure(ValidationError("Enter an amount above zero.")) } + guard stepped <= limits.maxBolus else { return .failure(ValidationError(exceedsMaxBolus(stepped, limits))) } + return .success(.bolus(units: stepped)) + + case let (.loop, .meal(carbs, fat, protein, bolus, consumedAt)): + guard fat == 0, protein == 0, bolus == 0 else { return .failure(ValidationError("Loop accepts carbs only from the watch.")) } + guard carbs > 0 else { return .failure(ValidationError("Enter an amount above zero.")) } + guard carbs <= limits.maxCarbs else { return .failure(ValidationError(exceeds("Carbs", carbs, limits.maxCarbs))) } + guard consumedAt >= now.addingTimeInterval(-loopMaxPastHours * 3600) else { + return .failure(ValidationError("Time must be within the prior \(Int(loopMaxPastHours)) hours.")) + } + guard consumedAt <= now.addingTimeInterval(loopMaxFutureHours * 3600) else { + return .failure(ValidationError("Time must be within the next \(Int(loopMaxFutureHours)) hour.")) + } + return .success(action) + + case let (.trio, .meal(carbs, fat, protein, bolus, consumedAt)): + guard limits.mealWithFatProtein || (fat == 0 && protein == 0) else { + return .failure(ValidationError("Meal with Fat/Protein is turned off in LoopFollow's Remote settings.")) + } + guard limits.mealWithBolus || bolus == 0 else { + return .failure(ValidationError("Meal with Bolus is turned off in LoopFollow's Remote settings.")) + } + guard carbs >= 0, fat >= 0, protein >= 0, carbs > 0 || fat > 0 || protein > 0 else { + return .failure(ValidationError("Enter carbs, fat or protein.")) + } + guard carbs <= limits.maxCarbs else { return .failure(ValidationError(exceeds("Carbs", carbs, limits.maxCarbs))) } + guard fat <= limits.maxFat else { return .failure(ValidationError(exceeds("Fat", fat, limits.maxFat))) } + guard protein <= limits.maxProtein else { return .failure(ValidationError(exceeds("Protein", protein, limits.maxProtein))) } + let steppedBolus = roundedToStep(bolus, step: limits.bolusIncrement) + guard steppedBolus <= limits.maxBolus else { return .failure(ValidationError(exceedsMaxBolus(steppedBolus, limits))) } + return .success(.meal(carbs: carbs, fat: fat, protein: protein, bolus: steppedBolus, consumedAt: consumedAt)) + + case (.loop, .startTempTarget), (.loop, .cancelTempTarget): + return .failure(ValidationError("Temp targets are not available for Loop.")) + + default: + return .success(action) + } + } + + /// Rounds down to the bolus increment, as the phone's Trio bolus screen does. + static func roundedToStep(_ value: Double, step: Double) -> Double { + let step = max(0.001, step) + let stepped = ((value / step) + 1e-10).rounded(.down) * step + return (stepped * 1000).rounded() / 1000 + } + + private static func exceedsMaxBolus(_ units: Double, _ limits: Limits) -> String { + "\(format(units)) U exceeds the maximum bolus of \(format(limits.maxBolus)) U set in LoopFollow." + } + + private static func exceeds(_ what: String, _ grams: Double, _ max: Double) -> String { + "\(what) (\(Int(grams)) g) exceeds the maximum of \(Int(max)) g set in LoopFollow." + } + + private static func format(_ units: Double) -> String { + let nf = NumberFormatter() + nf.numberStyle = .decimal + nf.minimumFractionDigits = 0 + nf.maximumFractionDigits = 3 + return nf.string(from: NSNumber(value: units)) ?? "\(units)" + } + + struct ValidationError: Error, Equatable { + let message: String + + init(_ message: String) { + self.message = message + } + } +} + +extension WatchCommand { + /// Trio treats a missing time as now; within a minute of now counts as now, as in the phone's meal screen. + static func trioScheduledTime(_ consumedAt: Date, now: Date = Date()) -> Date? { + abs(consumedAt.timeIntervalSince(now)) < 60 ? nil : consumedAt.dateUsingCurrentSeconds() + } +} diff --git a/RemoteShared/WatchCredentials.swift b/RemoteShared/WatchCredentials.swift new file mode 100644 index 000000000..4d3a30d9d --- /dev/null +++ b/RemoteShared/WatchCredentials.swift @@ -0,0 +1,64 @@ +// LoopFollow +// WatchCredentials.swift + +import Foundation + +/// What the watch needs to keep working without the phone: read access to the +/// data source and, when commands from the watch are allowed, what the phone +/// itself uses to send them. Sent with `transferUserInfo` only when it changes, +/// and kept in the watch's Keychain. +struct WatchCredentials: Codable, Equatable { + static let userInfoKey = "watchCredentials" + /// Watch → phone: asks the phone to send its credentials again. + static let requestKey = "requestCredentials" + + var nightscoutURL: String + var nightscoutToken: String + var dexcomUsername: String + var dexcomPassword: String + /// "US" or anything else for outside the US, as in the phone's Dexcom settings. + var dexcomServer: String + /// Present only when remote commands from the watch are allowed. + var remote: Remote? + + struct Remote: Codable, Equatable { + var kind: WatchRemoteConfig.Kind + /// Push target: the Loop or Trio app. + var deviceToken: String + var bundleID: String + var production: Bool + /// The key the phone signs commands with (LoopFollow's own key when both apps share a team). + var apnsKey: String + var keyID: String + var teamID: String + /// Trio Remote Control. + var user: String + var sharedSecret: String + /// Loop: the one-time-password QR code URL. + var loopQRCodeURL: String + /// Lets Loop or Trio confirm a command to the LoopFollow app on the phone. + var returnNotification: ReturnNotificationInfo? + } + + var hasDataSource: Bool { + !nightscoutURL.isEmpty || (!dexcomUsername.isEmpty && !dexcomPassword.isEmpty) + } +} + +/// Picks the key that signs commands to the Loop or Trio app: LoopFollow's own +/// key when both apps belong to the same team or no remote team is set, +/// otherwise the remote app's key. +enum RemoteSigningKey { + static func resolve( + lfTeamID: String, + remoteTeamID: String, + lfKey: (key: String, keyID: String), + remoteKey: (key: String, keyID: String) + ) -> (key: String, keyID: String, teamID: String) { + let sameTeam = !lfTeamID.isEmpty && !remoteTeamID.isEmpty && lfTeamID == remoteTeamID + if sameTeam || remoteTeamID.isEmpty { + return (lfKey.key, lfKey.keyID, lfTeamID) + } + return (remoteKey.key, remoteKey.keyID, remoteTeamID) + } +} diff --git a/Shared/GlucoseSnapshot.swift b/Shared/GlucoseSnapshot.swift index b2a015d17..a8c80038e 100644 --- a/Shared/GlucoseSnapshot.swift +++ b/Shared/GlucoseSnapshot.swift @@ -363,5 +363,27 @@ extension GlucoseSnapshot { let raw = try container.decode(String.self) self = Trend(rawValue: raw) ?? .unknown } + + /// Maps a Nightscout or Dexcom direction string, e.g. "FortyFiveUp". + init(code: String?) { + let raw = code?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? "" + if raw.contains("doubleup") || raw.contains("rapidrise") || raw == "up2" || raw == "upfast" { + self = .upFast + } else if raw.contains("fortyfiveup") { + self = .upSlight + } else if raw.contains("singleup") || raw == "up" || raw == "up1" || raw == "rising" { + self = .up + } else if raw.contains("flat") || raw == "steady" || raw == "none" { + self = .flat + } else if raw.contains("doubledown") || raw.contains("rapidfall") || raw == "down2" || raw == "downfast" { + self = .downFast + } else if raw.contains("fortyfivedown") { + self = .downSlight + } else if raw.contains("singledown") || raw == "down" || raw == "down1" || raw == "falling" { + self = .down + } else { + self = .unknown + } + } } } diff --git a/WatchShared/WatchCommand.swift b/WatchShared/WatchCommand.swift index 96f932563..5668fb463 100644 --- a/WatchShared/WatchCommand.swift +++ b/WatchShared/WatchCommand.swift @@ -32,6 +32,30 @@ struct WatchCommand: Codable, Equatable { } } +extension WatchCommand.Action { + /// Short name for logs. + var name: String { + switch self { + case .bolus: "bolus" + case .meal: "meal" + case .startOverride: "startOverride" + case .cancelOverride: "cancelOverride" + case .startTempTarget: "startTempTarget" + case .cancelTempTarget: "cancelTempTarget" + case .snoozeAlarm: "snoozeAlarm" + } + } + + /// Whether the command can deliver insulin. + var deliversInsulin: Bool { + switch self { + case .bolus: true + case let .meal(_, _, _, bolus, _): bolus > 0 + default: false + } + } +} + struct WatchCommandReply: Codable, Equatable { static let messageKey = "watchCommandReply" From 70bdf2e81519f40d4b73eb27f4c9e9ccabeda403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:11:13 +0200 Subject: [PATCH 10/25] Let the watch fetch from Nightscout or Dexcom and send commands directly while the phone is out of reach --- .../Remote/LoopAPNS/LoopAPNSService.swift | 68 +---- LoopFollow/Watch/WatchStateBuilder.swift | 24 +- LoopFollowWatch/DirectCommandSender.swift | 172 +++++++++++ LoopFollowWatch/LoopFollowWatchApp.swift | 19 +- LoopFollowWatch/StandaloneFetcher.swift | 266 ++++++++++++++++++ LoopFollowWatch/WatchSession.swift | 121 ++++++-- RemoteShared/LoopAPNSCommands.swift | 44 +++ RemoteShared/WatchCommandValidator.swift | 15 + Tests/WatchProtocolTests.swift | 2 + WatchShared/WatchState.swift | 20 +- 10 files changed, 656 insertions(+), 95 deletions(-) create mode 100644 LoopFollowWatch/DirectCommandSender.swift create mode 100644 LoopFollowWatch/StandaloneFetcher.swift diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift index e9876fc3b..0bd67521a 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift @@ -462,47 +462,7 @@ class LoopAPNSService { return } - // Create APNS notification payload (matching Loop's expected format) - let now = Date() - let expiration = Date(timeIntervalSinceNow: 5 * 60) // 5 minutes from now - - // Create alert text (matching Nightscout's format) - var alertText = "\(presetName) Temporary Override" - if let duration = duration, duration > 0 { - let hours = Int(duration / 3600) - let minutes = Int((duration.truncatingRemainder(dividingBy: 3600)) / 60) - if hours > 0 { - alertText += " (\(hours)h \(minutes)m)" - } else { - alertText += " (\(minutes)m)" - } - } - - var payload: [String: Any] = [ - "override-name": presetName, - "remote-address": "LoopFollow", - "entered-by": "LoopFollow", - "sent-at": LoopAPNSCommands.formatDateForAPNS(now), - "expiration": LoopAPNSCommands.formatDateForAPNS(expiration), - "alert": alertText, - ] - - if let duration = duration { - payload["override-duration-minutes"] = Int(duration / 60) - } - - // For override commands, we can include return notification info unencrypted - // since override commands don't require OTP validation in Loop - if let returnInfo = ReturnNotificationInfo.loopFollow() { - payload["return_notification"] = [ - "production_environment": returnInfo.productionEnvironment, - "device_token": returnInfo.deviceToken, - "bundle_id": returnInfo.bundleId, - "team_id": returnInfo.teamId, - "key_id": returnInfo.keyId, - "apns_key": returnInfo.apnsKey, - ] - } + let payload = LoopAPNSCommands.overridePayload(presetName: presetName, duration: duration, returnInfo: ReturnNotificationInfo.loopFollow()) // Send the notification using the existing APNS infrastructure let creds = effectiveCredentials() @@ -534,31 +494,7 @@ class LoopAPNSService { return } - // Create APNS notification payload (matching Loop's expected format) - let now = Date() - let expiration = Date(timeIntervalSinceNow: 5 * 60) // 5 minutes from now - - var payload: [String: Any] = [ - "cancel-temporary-override": "true", - "remote-address": "LoopFollow", - "entered-by": "LoopFollow", - "sent-at": LoopAPNSCommands.formatDateForAPNS(now), - "expiration": LoopAPNSCommands.formatDateForAPNS(expiration), - "alert": "Cancel Temporary Override", - ] - - // For override commands, we can include return notification info unencrypted - // since override commands don't require OTP validation in Loop - if let returnInfo = ReturnNotificationInfo.loopFollow() { - payload["return_notification"] = [ - "production_environment": returnInfo.productionEnvironment, - "device_token": returnInfo.deviceToken, - "bundle_id": returnInfo.bundleId, - "team_id": returnInfo.teamId, - "key_id": returnInfo.keyId, - "apns_key": returnInfo.apnsKey, - ] - } + let payload = LoopAPNSCommands.cancelOverridePayload(returnInfo: ReturnNotificationInfo.loopFollow()) // Send the notification using the existing APNS infrastructure let creds = effectiveCredentials() diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index c4d1b38bc..e70ea120e 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -49,6 +49,8 @@ enum WatchStateBuilder { return WatchState( builtAt: now, + source: .phone, + phoneBuiltAt: now, snapshot: GlucoseSnapshotBuilder.build(from: StorageCurrentGlucoseStateProvider()), readings: readings, predictions: predictions, @@ -164,7 +166,12 @@ enum WatchStateBuilder { } parts.append(durationText(seconds: TimeInterval(override.duration ?? 0))) let symbol = override.symbol.isEmpty ? "" : "\(override.symbol) " - return WatchPreset(id: override.name, name: symbol + override.name, detail: parts.joined(separator: ", ")) + return WatchPreset( + id: override.name, + name: symbol + override.name, + detail: parts.joined(separator: ", "), + durationSeconds: TimeInterval(override.duration ?? 0) + ) } } @@ -178,7 +185,12 @@ enum WatchStateBuilder { parts.append(Localizer.formatQuantity(target)) } parts.append(durationText(seconds: (override.duration ?? 0) * 60)) - return WatchPreset(id: override.name, name: override.name, detail: parts.joined(separator: ", ")) + return WatchPreset( + id: override.name, + name: override.name, + detail: parts.joined(separator: ", "), + durationSeconds: (override.duration ?? 0) * 60 + ) } } @@ -186,7 +198,13 @@ enum WatchStateBuilder { TempTargetPresetManager.shared.presets.map { preset in let target = Localizer.formatQuantity(preset.target) let duration = durationText(seconds: preset.duration.doubleValue(for: .second())) - return WatchPreset(id: preset.id.uuidString, name: preset.name, detail: "\(target), \(duration)") + return WatchPreset( + id: preset.id.uuidString, + name: preset.name, + detail: "\(target), \(duration)", + durationSeconds: preset.duration.doubleValue(for: .second()), + targetMgdl: preset.target.doubleValue(for: .milligramsPerDeciliter) + ) } } diff --git a/LoopFollowWatch/DirectCommandSender.swift b/LoopFollowWatch/DirectCommandSender.swift new file mode 100644 index 000000000..b2320524f --- /dev/null +++ b/LoopFollowWatch/DirectCommandSender.swift @@ -0,0 +1,172 @@ +// LoopFollow +// DirectCommandSender.swift + +import Foundation + +/// Sends a command from the watch straight to Loop or Trio when the phone is +/// out of reach, with the credentials and guardrails the phone last sent. +enum DirectCommandSender { + /// At least this long between two insulin commands sent from the watch. + static let insulinInterval: TimeInterval = 60 + + private static let lastInsulinKey = "directLastInsulinAt" + private static let lastLoopOTPKey = "directLastLoopOTP" + + static func send(_ requested: WatchCommand.Action, credentials: WatchCredentials, config: WatchRemoteConfig) async -> WatchCommandReply { + guard let remote = credentials.remote, remote.kind == config.kind else { + return WatchCommandReply(success: false, message: "iPhone not reachable, and commands from the watch alone are not set up. Open LoopFollow on the iPhone once.") + } + + let action: WatchCommand.Action + switch WatchCommandValidator.validate(requested, kind: remote.kind, limits: config.limits) { + case let .failure(error): return WatchCommandReply(success: false, message: error.message) + case let .success(validated): action = validated + } + + if action.deliversInsulin, let last = UserDefaults.standard.object(forKey: lastInsulinKey) as? Date, + Date().timeIntervalSince(last) < insulinInterval + { + return WatchCommandReply(success: false, message: "Insulin was sent from the watch less than a minute ago. Check Loop or Trio before sending more.") + } + + let result: (Bool, String?) + switch remote.kind { + case .trio: result = await sendTrio(action, remote: remote, config: config) + case .loop: result = await sendLoop(action, remote: remote, config: config) + } + + LogManager.shared.log(category: .apns, message: "Direct command \(action.name) finished: success=\(result.0) \(result.1 ?? "")") + guard result.0 else { + return WatchCommandReply(success: false, message: result.1 ?? "Failed to send the command.") + } + if action.deliversInsulin { + UserDefaults.standard.set(Date(), forKey: lastInsulinKey) + } + return WatchCommandReply(success: true, message: "Sent from the watch without the iPhone. Check Loop or Trio for the result.") + } + + // MARK: - Trio + + private static func sendTrio(_ action: WatchCommand.Action, remote: WatchCredentials.Remote, config: WatchRemoteConfig) async -> (Bool, String?) { + var payload = CommandPayload(user: remote.user, commandType: .bolus, timestamp: Date().timeIntervalSince1970, returnNotification: remote.returnNotification) + switch action { + case let .bolus(units): + payload.bolusAmount = Decimal(units) + case let .meal(carbs, fat, protein, bolus, consumedAt): + payload.commandType = .meal + payload.carbs = carbs > 0 ? Int(carbs) : nil + payload.fat = fat > 0 ? Int(fat) : nil + payload.protein = protein > 0 ? Int(protein) : nil + payload.bolusAmount = bolus > 0 ? Decimal(bolus) : nil + payload.scheduledTime = WatchCommand.trioScheduledTime(consumedAt)?.timeIntervalSince1970 + case let .startOverride(presetID): + payload.commandType = .startOverride + payload.overrideName = presetID + case .cancelOverride: + payload.commandType = .cancelOverride + case let .startTempTarget(presetID): + guard let preset = config.tempTargetPresets.first(where: { $0.id == presetID }), let target = preset.targetMgdl else { + return (false, "The temp target preset was not found.") + } + payload.commandType = .tempTarget + payload.target = Int(target) + payload.duration = Int(preset.durationSeconds / 60) + case .cancelTempTarget: + payload.commandType = .cancelTempTarget + case .snoozeAlarm: + return (false, "Snoozing needs the iPhone.") + } + + guard let messenger = SecureMessenger(sharedSecret: remote.sharedSecret) else { + return (false, "Failed to initialize security module. Check shared secret.") + } + do { + let message = try EncryptedPushMessage(encryptedData: messenger.encrypt(payload), commandType: payload.commandType) + let body = try JSONEncoder().encode(message) + return await deliver(body: body, remote: remote, expiration: "600", collapseID: payload.apnsCollapseID) + } catch { + return (false, "Failed to encode or encrypt push message: \(error.localizedDescription)") + } + } + + // MARK: - Loop + + private static func sendLoop(_ action: WatchCommand.Action, remote: WatchCredentials.Remote, config: WatchRemoteConfig) async -> (Bool, String?) { + var payload: [String: Any] + var usedOTP: String? + + switch action { + case let .bolus(units): + guard let otp = loopOTP(remote) else { return (false, otpUnavailable) } + payload = LoopAPNSCommands.bolusCommandPayload(LoopAPNSPayload(type: .bolus, bolusAmount: units, otp: otp)) + usedOTP = otp + case let .meal(carbs, _, _, _, consumedAt): + guard let otp = loopOTP(remote) else { return (false, otpUnavailable) } + payload = LoopAPNSCommands.carbsCommandPayload(LoopAPNSPayload( + type: .carbs, + carbsAmount: carbs, + absorptionTime: WatchCommandValidator.loopAbsorptionHours, + consumedDate: consumedAt.dateUsingCurrentSeconds(), + otp: otp + )) + usedOTP = otp + case let .startOverride(presetID): + guard let preset = config.overridePresets.first(where: { $0.id == presetID }) else { + return (false, "The override preset was not found.") + } + payload = LoopAPNSCommands.overridePayload( + presetName: presetID, + duration: preset.durationSeconds > 0 ? preset.durationSeconds : nil, + returnInfo: remote.returnNotification + ) + case .cancelOverride: + payload = LoopAPNSCommands.cancelOverridePayload(returnInfo: remote.returnNotification) + case .startTempTarget, .cancelTempTarget, .snoozeAlarm: + return (false, "This command is not available for Loop.") + } + + if let usedOTP, let returnInfo = remote.returnNotification, + let messenger = OTPSecureMessenger(otpCode: usedOTP), + let encrypted = try? messenger.encrypt(returnInfo) + { + payload["encrypted_return_notification"] = encrypted + } + + do { + let body = try LoopAPNSCommands.apnsBody(payload) + let result = await deliver(body: body, remote: remote) + if result.0, let usedOTP { + UserDefaults.standard.set(usedOTP, forKey: lastLoopOTPKey) + } + return result + } catch { + return (false, "Failed to serialize APNS payload: \(error.localizedDescription)") + } + } + + private static let otpUnavailable = "No unused one-time code right now. Wait for the next code (up to 30 seconds) and try again." + + /// The current one-time code, unless the watch already used it: Loop rejects a code it has seen. + private static func loopOTP(_ remote: WatchCredentials.Remote) -> String? { + guard let otp = TOTPGenerator.extractOTPFromURL(remote.loopQRCodeURL) else { return nil } + return otp == UserDefaults.standard.string(forKey: lastLoopOTPKey) ? nil : otp + } + + private static func deliver(body: Data, remote: WatchCredentials.Remote, expiration: String? = nil, collapseID: String? = nil) async -> (Bool, String?) { + await withCheckedContinuation { continuation in + APNSDelivery.send( + body: body, + deviceToken: remote.deviceToken, + topic: remote.bundleID, + production: remote.production, + keyId: remote.keyID, + teamId: remote.teamID, + apnsKey: remote.apnsKey, + expiration: expiration, + collapseID: collapseID + ) { success, error in + continuation.resume(returning: (success, error)) + } + } + } +} diff --git a/LoopFollowWatch/LoopFollowWatchApp.swift b/LoopFollowWatch/LoopFollowWatchApp.swift index 595ce0b9a..24d7deaf0 100644 --- a/LoopFollowWatch/LoopFollowWatchApp.swift +++ b/LoopFollowWatch/LoopFollowWatchApp.swift @@ -10,20 +10,29 @@ struct LoopFollowWatchApp: App { init() { WatchSession.shared.activate() + WatchSession.shared.scheduleBackgroundRefresh() } var body: some Scene { WindowGroup { ContentView() .environmentObject(session) - } - .onChange(of: scenePhase) { _, phase in - if phase == .active { - session.requestState() - } + .task(id: scenePhase) { + guard scenePhase == .active else { return } + session.requestState() + // While open, check every minute whether the watch has to fetch on its own. + while !Task.isCancelled { + await session.refreshIfNeeded() + try? await Task.sleep(nanoseconds: 60_000_000_000) + } + } } .backgroundTask(.watchConnectivity) { await WatchSession.shared.waitForPendingContent() } + .backgroundTask(.appRefresh(WatchSession.backgroundRefreshID)) { + await WatchSession.shared.refreshIfNeeded() + await WatchSession.shared.scheduleBackgroundRefresh() + } } } diff --git a/LoopFollowWatch/StandaloneFetcher.swift b/LoopFollowWatch/StandaloneFetcher.swift new file mode 100644 index 000000000..4b8f201bd --- /dev/null +++ b/LoopFollowWatch/StandaloneFetcher.swift @@ -0,0 +1,266 @@ +// LoopFollow +// StandaloneFetcher.swift + +import Foundation + +/// Builds the watch state straight from Nightscout or Dexcom while the phone is +/// out of reach. What only the phone knows (info rows, presets, guardrails, +/// thresholds) is kept from the last state the phone sent. +enum StandaloneFetcher { + enum FetchError: LocalizedError { + case noDataSource + case badResponse + + var errorDescription: String? { + switch self { + case .noDataSource: "No Nightscout or Dexcom account on the watch yet." + case .badResponse: "Unexpected response from the data source." + } + } + } + + private static let history: TimeInterval = 24 * 3600 + /// How far ahead predictions are drawn. + private static let predictionPoints = 36 + + static func fetch(credentials: WatchCredentials, base: WatchState?, now: Date = Date()) async throws -> WatchState { + if !credentials.nightscoutURL.isEmpty { + return try await fetchNightscout(credentials: credentials, base: base, now: now) + } + if !credentials.dexcomUsername.isEmpty, !credentials.dexcomPassword.isEmpty { + return try await fetchDexcom(credentials: credentials, base: base, now: now) + } + throw FetchError.noDataSource + } + + // MARK: - Nightscout + + private static func fetchNightscout(credentials: WatchCredentials, base: WatchState?, now: Date) async throws -> WatchState { + let since = now.addingTimeInterval(-history) + async let entries = get(credentials, "/api/v1/entries.json", [ + "find[date][$gte]": String(Int(since.timeIntervalSince1970 * 1000)), + "find[type][$ne]": "cal", + "count": "400", + ]) + async let deviceStatus = get(credentials, "/api/v1/devicestatus.json", ["count": "1"]) + async let treatments = get(credentials, "/api/v1/treatments.json", [ + "find[created_at][$gte]": iso.string(from: since), + "count": "500", + ]) + + let glucose = try JSONDecoder().decode([ShareGlucoseData].self, from: await entries) + .filter { $0.sgv > 0 } + .sorted { $0.date < $1.date } + let status = (try? JSONSerialization.jsonObject(with: await deviceStatus) as? [[String: Any]])?.first + let treatmentList = (try? JSONSerialization.jsonObject(with: await treatments) as? [[String: Any]]) ?? [] + + return build(glucose: glucose, status: status.map { parseDeviceStatus($0, base: base) }, treatments: treatmentList, base: base, now: now) + } + + private static func get(_ credentials: WatchCredentials, _ path: String, _ parameters: [String: String]) async throws -> Data { + var components = URLComponents(string: credentials.nightscoutURL) + components?.path = path + var items = parameters.map { URLQueryItem(name: $0.key, value: $0.value) } + if !credentials.nightscoutToken.isEmpty { + items.append(URLQueryItem(name: "token", value: credentials.nightscoutToken)) + } + components?.queryItems = items + guard let url = components?.url else { throw FetchError.badResponse } + var request = URLRequest(url: url) + request.timeoutInterval = 20 + let (data, response) = try await URLSession.shared.data(for: request) + guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badResponse } + return data + } + + // MARK: - Dexcom + + private static func fetchDexcom(credentials: WatchCredentials, base: WatchState?, now: Date) async throws -> WatchState { + let server = credentials.dexcomServer == "US" ? KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue + let client = ShareClient(username: credentials.dexcomUsername, password: credentials.dexcomPassword, shareServer: server) + let glucose: [ShareGlucoseData] = try await withCheckedThrowingContinuation { continuation in + client.fetchData(288) { error, data in + if let data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: error ?? FetchError.badResponse) + } + } + } + return build(glucose: glucose.sorted { $0.date < $1.date }, status: nil, treatments: [], base: base, now: now) + } + + // MARK: - Building + + struct LoopStatus { + var loopAt: Date? + var iob: Double? + var cob: Double? + var recommendedBolus: Double? + var predictions: [WatchPredictionLine] = [] + var cone: [WatchConePoint] = [] + } + + private static func build(glucose: [ShareGlucoseData], status: LoopStatus?, treatments: [[String: Any]], base: WatchState?, now: Date) -> WatchState { + let cutoff = now.timeIntervalSince1970 - history + let readings = glucose.filter { $0.date >= cutoff }.map { WatchReading(t: $0.date, v: Double($0.sgv)) } + let latest = glucose.last + let previous = glucose.dropLast().last + + let active = activeAdjustments(treatments, now: now) + var snapshot: GlucoseSnapshot? + if let latest { + let loopAge = status?.loopAt.map { now.timeIntervalSince($0) } + snapshot = GlucoseSnapshot( + glucose: Double(latest.sgv), + delta: previous.map { Double(latest.sgv - $0.sgv) } ?? 0, + trend: GlucoseSnapshot.Trend(code: latest.direction), + updatedAt: Date(timeIntervalSince1970: latest.date), + iob: status?.iob, + cob: status?.cob, + projected: nil, + override: active.override, + tempTargetMgdl: active.tempTargetMgdl, + recBolus: status?.recommendedBolus, + unit: base?.snapshot?.unit ?? .mgdl, + isNotLooping: loopAge.map { $0 >= 15 * 60 } ?? false + ) + } + + var remote = base?.remote + if remote != nil { + remote?.recommendedBolus = status?.recommendedBolus + remote?.recommendedBolusAt = status?.loopAt + } + + return WatchState( + builtAt: now, + source: .watch, + phoneBuiltAt: base?.phoneBuiltAt ?? .distantPast, + snapshot: snapshot, + readings: readings, + predictions: status?.predictions ?? [], + cone: status?.cone ?? [], + boluses: treatments.compactMap { reading($0, key: "insulin", cutoff: cutoff) }.sorted { $0.t < $1.t }, + carbs: treatments.compactMap { reading($0, key: "carbs", cutoff: cutoff) }.sorted { $0.t < $1.t }, + lowMgdl: base?.lowMgdl ?? 70, + highMgdl: base?.highMgdl ?? 180, + colorBGText: base?.colorBGText ?? true, + infoRows: base?.infoRows ?? [], + lastLoopAt: status?.loopAt, + device: base?.device ?? "", + alarm: nil, + remote: remote + ) + } + + /// Reads the loop's latest report the same way the phone does: `loop` for Loop, `openaps` for Trio. + static func parseDeviceStatus(_ status: [String: Any], base: WatchState?) -> LoopStatus { + var result = LoopStatus() + if let loop = status["loop"] as? [String: Any] { + result.loopAt = date(loop["timestamp"]) + result.iob = (loop["iob"] as? [String: Any])?["iob"] as? Double + result.cob = (loop["cob"] as? [String: Any])?["cob"] as? Double + result.recommendedBolus = loop["recommendedBolus"] as? Double + if let predicted = loop["predicted"] as? [String: Any], + let values = predicted["values"] as? [Double], + let start = date(predicted["startDate"]) + { + result.predictions = [WatchPredictionLine(kind: .loop, points: points(values, start: start))] + } + } else if let openaps = status["openaps"] as? [String: Any] { + let suggested = openaps["suggested"] as? [String: Any] ?? openaps["enacted"] as? [String: Any] ?? [:] + result.loopAt = date(suggested["deliverAt"] ?? suggested["timestamp"]) + let iob = openaps["iob"] as? [String: Any] ?? (openaps["iob"] as? [[String: Any]])?.first + result.iob = iob?["iob"] as? Double ?? suggested["IOB"] as? Double + result.cob = suggested["COB"] as? Double + result.recommendedBolus = openaps["recommendedBolus"] as? Double + if let predBGs = suggested["predBGs"] as? [String: [Double]], let start = result.loopAt { + let lines: [(WatchPredictionLine.Kind, String)] = [(.zt, "ZT"), (.iob, "IOB"), (.cob, "COB"), (.uam, "UAM")] + let series = lines.compactMap { kind, key in predBGs[key].map { (kind, points($0, start: start)) } } + // The phone shows Trio's forecast as a cone or as lines; keep what it showed last. + if base?.cone.isEmpty == false { + result.cone = cone(series.map(\.1)) + } else { + result.predictions = series.map { WatchPredictionLine(kind: $0.0, points: $0.1) } + } + } + } + return result + } + + private static func points(_ values: [Double], start: Date) -> [WatchReading] { + values.prefix(predictionPoints + 1).enumerated().map { index, value in + WatchReading(t: start.timeIntervalSince1970 + Double(index) * 300, v: value) + } + } + + /// Same construction as the phone: the shortest series sets the length, min and max per step. + private static func cone(_ series: [[WatchReading]]) -> [WatchConePoint] { + guard let length = series.map(\.count).min(), length > 0 else { return [] } + return (0 ..< length).map { index in + let values = series.map { $0[index].v } + var low = values.min() ?? 0 + var high = values.max() ?? 0 + if low == high { + low -= 1 + high += 1 + } + return WatchConePoint(t: series[0][index].t, low: low, high: high) + } + } + + private static func reading(_ treatment: [String: Any], key: String, cutoff: TimeInterval) -> WatchReading? { + guard let value = treatment[key] as? Double, value > 0, + let at = date(treatment["timestamp"] ?? treatment["created_at"]), + at.timeIntervalSince1970 >= cutoff + else { return nil } + return WatchReading(t: at.timeIntervalSince1970, v: value) + } + + /// The override and temp target that are running now, from the latest of each in the treatments. + private static func activeAdjustments(_ treatments: [[String: Any]], now: Date) -> (override: String?, tempTargetMgdl: Double?) { + func latest(_ eventType: String) -> (start: Date, entry: [String: Any])? { + treatments + .filter { $0["eventType"] as? String == eventType } + .compactMap { entry in date(entry["timestamp"] ?? entry["created_at"]).map { ($0, entry) } } + .max { $0.0 < $1.0 } + } + + var override: String? + if let (start, entry) = latest("Temporary Override") { + let minutes = entry["duration"] as? Double ?? 0 + let indefinite = minutes == 0 || (entry["durationType"] as? String) == "indefinite" + if start <= now, indefinite || now < start.addingTimeInterval(minutes * 60) { + override = entry["reason"] as? String ?? entry["notes"] as? String ?? "Override" + } + } + + var tempTarget: Double? + if let (start, entry) = latest("Temporary Target") { + let minutes = entry["duration"] as? Double ?? 0 + if minutes > 0, start <= now, now < start.addingTimeInterval(minutes * 60) { + tempTarget = entry["targetTop"] as? Double ?? entry["targetBottom"] as? Double + } + } + return (override, tempTarget) + } + + // MARK: - Dates + + private static let iso: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + }() + + private static let isoNoFraction = ISO8601DateFormatter() + + private static func date(_ value: Any?) -> Date? { + if let number = value as? Double { + return Date(timeIntervalSince1970: number > 1e11 ? number / 1000 : number) + } + guard let string = value as? String else { return nil } + return iso.date(from: string) ?? isoNoFraction.date(from: string) + } +} diff --git a/LoopFollowWatch/WatchSession.swift b/LoopFollowWatch/WatchSession.swift index 6b7f81b5d..9f6e2ffb1 100644 --- a/LoopFollowWatch/WatchSession.swift +++ b/LoopFollowWatch/WatchSession.swift @@ -6,19 +6,32 @@ import WatchConnectivity import WatchKit import WidgetKit -/// Watch side of the phone link. Receives `WatchState` from the phone and sends -/// commands back for the phone to validate and relay. +/// Watch side of the phone link. The phone is the main source: it sends +/// `WatchState` and relays commands. While the phone's data is stale and the +/// phone does not answer, the watch fetches from Nightscout or Dexcom itself +/// and sends commands directly. @MainActor final class WatchSession: NSObject, ObservableObject { static let shared = WatchSession() + /// Phone data older than this hands over to the watch's own fetching. + static let phoneStaleAfter: TimeInterval = 10 * 60 + /// Minimum time between two fetches made by the watch itself. + private static let standaloneInterval: TimeInterval = 60 + @Published private(set) var state: WatchState? @Published private(set) var isReachable = false private var session: WCSession { WCSession.default } + private var credentials: WatchCredentials? + private var lastPhoneBuiltAt: Date + private var lastStandaloneAttempt: Date = .distantPast override private init() { - state = WatchStateStore.load() + let stored = WatchStateStore.load() + state = stored + lastPhoneBuiltAt = stored?.phoneBuiltAt ?? .distantPast + credentials = WatchKeychain.load() super.init() } @@ -28,22 +41,59 @@ final class WatchSession: NSObject, ObservableObject { session.activate() } + /// True while the phone's latest state is recent enough to rely on. + var isPhoneDataFresh: Bool { + Date().timeIntervalSince(lastPhoneBuiltAt) < Self.phoneStaleAfter + } + /// Asks the phone for fresh state; the application context covers the case /// where the phone is out of reach. func requestState() { guard session.activationState == .activated, session.isReachable else { return } session.sendMessage([WatchMessageKey.requestState: true], replyHandler: { reply in guard let data = reply[WatchState.contextKey] as? Data else { return } - Task { @MainActor in self.apply(data) } + Task { @MainActor in self.applyFromPhone(data) } }, errorHandler: nil) } - /// Sends a command to the phone and waits for its verdict. The phone has to be - /// reachable: a command is never queued for later delivery. + /// Fetches from Nightscout or Dexcom when the phone's data is stale and the phone does not answer. + func refreshIfNeeded() async { + guard !isPhoneDataFresh else { return } + if session.activationState == .activated, session.isReachable { + requestState() + try? await Task.sleep(nanoseconds: 8_000_000_000) + guard !isPhoneDataFresh else { return } + } + guard let credentials, credentials.hasDataSource, + Date().timeIntervalSince(lastStandaloneAttempt) >= Self.standaloneInterval + else { return } + lastStandaloneAttempt = Date() + do { + let fetched = try await StandaloneFetcher.fetch(credentials: credentials, base: state) + guard !isPhoneDataFresh else { return } + store(fetched) + LogManager.shared.log(category: .nightscout, message: "Standalone fetch: \(fetched.readings.count) readings") + } catch { + LogManager.shared.log(category: .nightscout, message: "Standalone fetch failed: \(error.localizedDescription)") + } + } + + /// Sends a command through the phone when it is reachable, otherwise directly. + /// Never both: a command that fails through the phone is not retried directly. func send(_ action: WatchCommand.Action) async -> WatchCommandReply { - guard session.activationState == .activated, session.isReachable else { + if session.activationState == .activated, session.isReachable { + return await sendThroughPhone(action) + } + if case .snoozeAlarm = action { + return WatchCommandReply(success: false, message: "Snoozing needs the iPhone, which is not reachable right now.") + } + guard let credentials, let config = state?.remote else { return WatchCommandReply(success: false, message: "iPhone not reachable. Open LoopFollow on the iPhone and keep it near the watch.") } + return await DirectCommandSender.send(action, credentials: credentials, config: config) + } + + private func sendThroughPhone(_ action: WatchCommand.Action) async -> WatchCommandReply { guard let data = try? JSONEncoder().encode(WatchCommand(action: action)) else { return WatchCommandReply(success: false, message: "Could not encode the command.") } @@ -70,19 +120,28 @@ final class WatchSession: NSObject, ObservableObject { } } - private static func complicationKey(_ state: WatchState) -> String { - let s = state.snapshot - return "\(s?.updatedAt.timeIntervalSince1970 ?? 0)|\(s?.unit.rawValue ?? "")|\(state.lowMgdl)|\(state.highMgdl)|\(state.colorBGText)" + static let backgroundRefreshID = "standalone" + + /// Schedules the next background check for stale phone data. + func scheduleBackgroundRefresh() { + WKApplication.shared().scheduleBackgroundRefresh( + withPreferredDate: Date().addingTimeInterval(15 * 60), + userInfo: Self.backgroundRefreshID as NSString + ) { _ in } } - private func apply(_ data: Data) { - guard let incoming = WatchState.decode(data) else { return } - if let current = state, current.builtAt > incoming.builtAt { return } + private func applyFromPhone(_ data: Data) { + guard let incoming = WatchState.decode(data), incoming.builtAt > lastPhoneBuiltAt else { return } + lastPhoneBuiltAt = incoming.builtAt + store(incoming) + } + private func store(_ incoming: WatchState) { + guard let encoded = incoming.encoded() else { return } let newAlarm = incoming.alarm != nil && incoming.alarm?.id != state?.alarm?.id let complicationChanged = Self.complicationKey(incoming) != state.map(Self.complicationKey) state = incoming - WatchStateStore.save(data) + WatchStateStore.save(encoded) // Widget reloads are budgeted, so only reload when something the complication shows has changed. if complicationChanged { WidgetCenter.shared.reloadAllTimelines() @@ -92,6 +151,27 @@ final class WatchSession: NSObject, ObservableObject { WKInterfaceDevice.current().play(.notification) } } + + private static func complicationKey(_ state: WatchState) -> String { + let s = state.snapshot + return "\(s?.updatedAt.timeIntervalSince1970 ?? 0)|\(s?.unit.rawValue ?? "")|\(state.lowMgdl)|\(state.highMgdl)|\(state.colorBGText)" + } + + private func receiveCredentials(_ data: Data) { + guard let received = try? JSONDecoder().decode(WatchCredentials.self, from: data) else { return } + credentials = received + WatchKeychain.save(received) + } + + /// Asks the phone for credentials when the watch has none, e.g. after a reinstall. + private func requestCredentialsIfMissing() { + guard credentials == nil, session.activationState == .activated else { return } + if session.isReachable { + session.sendMessage([WatchCredentials.requestKey: true], replyHandler: nil, errorHandler: nil) + } else { + session.transferUserInfo([WatchCredentials.requestKey: true]) + } + } } extension WatchSession: WCSessionDelegate { @@ -99,9 +179,10 @@ extension WatchSession: WCSessionDelegate { let context = session.receivedApplicationContext[WatchState.contextKey] as? Data let reachable = session.isReachable Task { @MainActor in - if let context { self.apply(context) } + if let context { self.applyFromPhone(context) } self.isReachable = reachable self.requestState() + self.requestCredentialsIfMissing() } } @@ -115,11 +196,15 @@ extension WatchSession: WCSessionDelegate { nonisolated func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { guard let data = applicationContext[WatchState.contextKey] as? Data else { return } - Task { @MainActor in self.apply(data) } + Task { @MainActor in self.applyFromPhone(data) } } nonisolated func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { - guard let data = userInfo[WatchState.contextKey] as? Data else { return } - Task { @MainActor in self.apply(data) } + let state = userInfo[WatchState.contextKey] as? Data + let credentials = userInfo[WatchCredentials.userInfoKey] as? Data + Task { @MainActor in + if let state { self.applyFromPhone(state) } + if let credentials { self.receiveCredentials(credentials) } + } } } diff --git a/RemoteShared/LoopAPNSCommands.swift b/RemoteShared/LoopAPNSCommands.swift index c01c2fd55..0473df59e 100644 --- a/RemoteShared/LoopAPNSCommands.swift +++ b/RemoteShared/LoopAPNSCommands.swift @@ -73,6 +73,50 @@ enum LoopAPNSCommands { /// Creates a properly formatted ISO8601 date string with milliseconds (matching Nightscout's format) /// - Parameter date: The date to format /// - Returns: Formatted date string like "2022-12-24T21:34:02.090Z" + /// Override commands need no one-time code, so the return block travels unencrypted. + static func overridePayload(presetName: String, duration: TimeInterval?, returnInfo: ReturnNotificationInfo?, now: Date = Date()) -> [String: Any] { + // Alert text matches Nightscout's format. + var alertText = "\(presetName) Temporary Override" + if let duration, duration > 0 { + let hours = Int(duration / 3600) + let minutes = Int(duration.truncatingRemainder(dividingBy: 3600) / 60) + alertText += hours > 0 ? " (\(hours)h \(minutes)m)" : " (\(minutes)m)" + } + var payload = overrideHeader(alert: alertText, now: now, returnInfo: returnInfo) + payload["override-name"] = presetName + if let duration { + payload["override-duration-minutes"] = Int(duration / 60) + } + return payload + } + + static func cancelOverridePayload(returnInfo: ReturnNotificationInfo?, now: Date = Date()) -> [String: Any] { + var payload = overrideHeader(alert: "Cancel Temporary Override", now: now, returnInfo: returnInfo) + payload["cancel-temporary-override"] = "true" + return payload + } + + private static func overrideHeader(alert: String, now: Date, returnInfo: ReturnNotificationInfo?) -> [String: Any] { + var payload: [String: Any] = [ + "remote-address": "LoopFollow", + "entered-by": "LoopFollow", + "sent-at": formatDateForAPNS(now), + "expiration": formatDateForAPNS(now.addingTimeInterval(5 * 60)), + "alert": alert, + ] + if let returnInfo { + payload["return_notification"] = [ + "production_environment": returnInfo.productionEnvironment, + "device_token": returnInfo.deviceToken, + "bundle_id": returnInfo.bundleId, + "team_id": returnInfo.teamId, + "key_id": returnInfo.keyId, + "apns_key": returnInfo.apnsKey, + ] + } + return payload + } + /// Wraps command fields in the push payload Loop expects (matching @parse/node-apn). static func apnsBody(_ payload: [String: Any]) throws -> Data { var body: [String: Any] = [ diff --git a/RemoteShared/WatchCommandValidator.swift b/RemoteShared/WatchCommandValidator.swift index 3bd475a81..15769a0b2 100644 --- a/RemoteShared/WatchCommandValidator.swift +++ b/RemoteShared/WatchCommandValidator.swift @@ -117,3 +117,18 @@ extension WatchCommand { abs(consumedAt.timeIntervalSince(now)) < 60 ? nil : consumedAt.dateUsingCurrentSeconds() } } + +extension WatchRemoteConfig { + /// The phone's guardrails as last sent to the watch. + var limits: WatchCommandValidator.Limits { + WatchCommandValidator.Limits( + maxBolus: maxBolus, + bolusIncrement: bolusIncrement, + maxCarbs: maxCarbs, + maxFat: maxFat, + maxProtein: maxProtein, + mealWithBolus: mealWithBolus, + mealWithFatProtein: mealWithFatProtein + ) + } +} diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index abd1176e2..71b08b6d8 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -11,6 +11,8 @@ struct WatchProtocolTests { private func state(alarm: WatchAlarm? = nil) -> WatchState { WatchState( builtAt: t0, + source: .phone, + phoneBuiltAt: t0, snapshot: GlucoseSnapshot(glucose: 142, delta: -3, trend: .downSlight, updatedAt: t0, iob: 1.2, cob: 15, projected: 120, unit: .mmol, isNotLooping: false), readings: [WatchReading(t: t0.timeIntervalSince1970 - 300, v: 145), WatchReading(t: t0.timeIntervalSince1970, v: 142)], predictions: [WatchPredictionLine(kind: .iob, points: [WatchReading(t: t0.timeIntervalSince1970 + 300, v: 138)])], diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index 2bc8b4019..fc3a3da95 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -9,8 +9,11 @@ import Foundation struct WatchState: Codable, Equatable { static let contextKey = "watchState" - /// When the phone built this state. + /// When this state was built, by the phone or by the watch itself. var builtAt: Date + var source: WatchDataSource + /// When the phone last built a state; info rows, presets and guardrails date from then. + var phoneBuiltAt: Date /// Latest glucose state, shared with the Live Activity. var snapshot: GlucoseSnapshot? /// BG readings for the last 24 hours, oldest first. @@ -46,6 +49,13 @@ struct WatchState: Codable, Equatable { } } +enum WatchDataSource: String, Codable { + /// Built and sent by the phone. + case phone + /// Fetched by the watch from Nightscout or Dexcom while the phone was out of reach. + case watch +} + struct WatchReading: Codable, Equatable { /// Unix epoch seconds. let t: TimeInterval @@ -101,6 +111,10 @@ struct WatchPreset: Codable, Equatable, Identifiable { let name: String /// Preformatted on the phone in the user's units, e.g. "120 %, 1 h". let detail: String + /// Raw values the watch needs to send the preset without the phone: the + /// duration (0 = indefinite) and, for temp targets, the target in mg/dL. + var durationSeconds: Double = 0 + var targetMgdl: Double? } struct WatchRemoteConfig: Codable, Equatable { @@ -120,8 +134,8 @@ struct WatchRemoteConfig: Codable, Equatable { /// Trio only: a meal may carry fat and protein. let mealWithFatProtein: Bool /// The AID's own recommendation, as shown in the phone's bolus screen. - let recommendedBolus: Double? - let recommendedBolusAt: Date? + var recommendedBolus: Double? + var recommendedBolusAt: Date? let overridePresets: [WatchPreset] /// Trio only. let tempTargetPresets: [WatchPreset] From 4a36ce2dca938b51af84e1a4ee57d6dc1f183727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:12:34 +0200 Subject: [PATCH 11/25] Show the phone's statistics on the watch and mark data fetched by the watch itself --- .../ViewControllers/MainViewController.swift | 2 +- LoopFollow/Watch/WatchSessionManager.swift | 7 +- LoopFollow/Watch/WatchStateBuilder.swift | 17 +++++ LoopFollowWatch/ContentView.swift | 3 + LoopFollowWatch/GlucosePage.swift | 30 +++++--- LoopFollowWatch/InfoPage.swift | 6 ++ LoopFollowWatch/StandaloneFetcher.swift | 1 + LoopFollowWatch/StatsPage.swift | 68 +++++++++++++++++++ LoopFollowWatch/WatchSession.swift | 5 ++ Tests/WatchProtocolTests.swift | 1 + WatchShared/WatchState.swift | 17 +++++ 11 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 LoopFollowWatch/StatsPage.swift diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 5712ffc5b..0b9514bc0 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -152,7 +152,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { view.backgroundColor = .systemBackground infoManager = InfoManager() - WatchSessionManager.shared.observe(infoManager) + WatchSessionManager.shared.observe(infoManager, stats: statsDisplayModel) let mainView = MainHomeView( chartModel: chartModel, diff --git a/LoopFollow/Watch/WatchSessionManager.swift b/LoopFollow/Watch/WatchSessionManager.swift index 8f8b14b38..6e0dcb4ab 100644 --- a/LoopFollow/Watch/WatchSessionManager.swift +++ b/LoopFollow/Watch/WatchSessionManager.swift @@ -104,11 +104,14 @@ final class WatchSessionManager: NSObject { LogManager.shared.log(category: .general, message: "[Watch] credentials sent") } - /// Sends the info table to the watch whenever one of its rows changes. - func observe(_ infoManager: InfoManager) { + /// Sends the info table and statistics to the watch whenever they change. + func observe(_ infoManager: InfoManager, stats: StatsDisplayModel) { infoManager.objectWillChange .sink { [weak self] in self?.requestUpdate(reason: "info") } .store(in: &cancellables) + stats.objectWillChange + .sink { [weak self] in self?.requestUpdate(reason: "stats") } + .store(in: &cancellables) } private func publish(reason: String) { diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index e70ea120e..74a35a94a 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -61,6 +61,7 @@ enum WatchStateBuilder { highMgdl: thresholds.high, colorBGText: Storage.shared.colorBGText.value, infoRows: infoRows(vc), + stats: vc.map { stats($0.statsDisplayModel) }, lastLoopAt: lastLoop, device: Storage.shared.device.value, alarm: currentAlarm(), @@ -91,6 +92,22 @@ enum WatchStateBuilder { return rows } + private static func stats(_ model: StatsDisplayModel) -> WatchStats { + WatchStats( + lowPercent: model.lowPercent, + inRangePercent: model.inRangePercent, + highPercent: model.highPercent, + avgBG: model.avgBG, + estA1CTitle: model.estA1CTitle, + estA1C: model.estA1C, + stdDevTitle: model.stdDevTitle, + stdDev: model.stdDev, + pieLow: model.pieLow, + pieRange: model.pieRange, + pieHigh: model.pieHigh + ) + } + private static func currentAlarm() -> WatchAlarm? { guard let id = Observable.shared.currentAlarm.value, let alarm = Storage.shared.alarms.value.first(where: { $0.id == id }) diff --git a/LoopFollowWatch/ContentView.swift b/LoopFollowWatch/ContentView.swift index 0601f7c02..95d14d8c0 100644 --- a/LoopFollowWatch/ContentView.swift +++ b/LoopFollowWatch/ContentView.swift @@ -45,6 +45,9 @@ struct ContentView: View { TabView { GlucosePage(state: state) ChartPage(state: state) + if let stats = state.stats { + StatsPage(state: state, stats: stats) + } InfoPage(state: state) } .tabViewStyle(.verticalPage) diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index 0d4f302a0..ae944dd27 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -75,17 +75,25 @@ struct GlucosePage: View { } } + /// Age of the reading, with a marker while the watch fetches on its own or cannot reach the phone. @ViewBuilder private func age(now: Date) -> some View { - if !session.isReachable { - Image(systemName: "iphone.slash") - .foregroundStyle(.orange) - .accessibilityLabel("iPhone not reachable") - } else if let snapshot = state.snapshot { - Text(WatchFormat.minutesAgo(snapshot.updatedAt, now: now)) - .font(.footnote) - .foregroundStyle(now.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter ? .red : .secondary) + HStack(spacing: 2) { + if state.source == .watch { + Image(systemName: "applewatch.radiowaves.left.and.right") + .foregroundStyle(.blue) + .accessibilityLabel("Fetched by the watch") + } else if !session.isReachable { + Image(systemName: "iphone.slash") + .foregroundStyle(.orange) + .accessibilityLabel("iPhone not reachable") + } + if let snapshot = state.snapshot { + Text(WatchFormat.minutesAgo(snapshot.updatedAt, now: now)) + .foregroundStyle(now.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter ? .red : .secondary) + } } + .font(.footnote) } @ViewBuilder @@ -129,7 +137,7 @@ private struct RemoteToolbar: View { } .tint(state.snapshot?.override != nil ? .green : nil) .accessibilityLabel("Overrides") - .disabled(!session.isReachable) + .disabled(!session.canSendCommands) Button { navigator.push(.treatments) @@ -140,7 +148,7 @@ private struct RemoteToolbar: View { .buttonStyle(.borderedProminent) .tint(.blue) .accessibilityLabel("Meal or bolus") - .disabled(!session.isReachable) + .disabled(!session.canSendCommands) if remote.kind == .trio { Button { @@ -150,7 +158,7 @@ private struct RemoteToolbar: View { } .tint(state.snapshot?.tempTargetMgdl != nil ? .green : nil) .accessibilityLabel("Temp Target") - .disabled(!session.isReachable) + .disabled(!session.canSendCommands) } else { Spacer() } diff --git a/LoopFollowWatch/InfoPage.swift b/LoopFollowWatch/InfoPage.swift index c5f1a3a71..c8a6d5826 100644 --- a/LoopFollowWatch/InfoPage.swift +++ b/LoopFollowWatch/InfoPage.swift @@ -10,6 +10,12 @@ struct InfoPage: View { var body: some View { ScrollView { VStack(spacing: 0) { + if state.source == .watch, !state.infoRows.isEmpty { + Text("From the iPhone at \(state.phoneBuiltAt.formatted(date: .omitted, time: .shortened))") + .font(.caption2) + .foregroundStyle(.orange) + .frame(maxWidth: .infinity, alignment: .leading) + } if state.infoRows.isEmpty { Text("No info rows are shown on the iPhone.") .font(.footnote) diff --git a/LoopFollowWatch/StandaloneFetcher.swift b/LoopFollowWatch/StandaloneFetcher.swift index 4b8f201bd..8b36dcc96 100644 --- a/LoopFollowWatch/StandaloneFetcher.swift +++ b/LoopFollowWatch/StandaloneFetcher.swift @@ -147,6 +147,7 @@ enum StandaloneFetcher { highMgdl: base?.highMgdl ?? 180, colorBGText: base?.colorBGText ?? true, infoRows: base?.infoRows ?? [], + stats: base?.stats, lastLoopAt: status?.loopAt, device: base?.device ?? "", alarm: nil, diff --git a/LoopFollowWatch/StatsPage.swift b/LoopFollowWatch/StatsPage.swift new file mode 100644 index 000000000..04ec64221 --- /dev/null +++ b/LoopFollowWatch/StatsPage.swift @@ -0,0 +1,68 @@ +// LoopFollow +// StatsPage.swift + +import Charts +import SwiftUI + +/// The statistics panel from under the phone's chart: the same pie and values. +struct StatsPage: View { + let state: WatchState + let stats: WatchStats + + var body: some View { + ScrollView { + VStack(spacing: 8) { + Chart(slices, id: \.0) { slice in + SectorMark(angle: .value("share", slice.1)) + .foregroundStyle(slice.2) + } + .chartLegend(.hidden) + .frame(height: 70) + .accessibilityHidden(true) + + Grid(horizontalSpacing: 4, verticalSpacing: 6) { + GridRow { + value("Low:", stats.lowPercent) + value("In Range:", stats.inRangePercent) + value("High:", stats.highPercent) + } + GridRow { + value("Avg BG:", stats.avgBG) + value(stats.estA1CTitle, stats.estA1C) + value(stats.stdDevTitle, stats.stdDev) + } + } + + if state.source == .watch { + Text("From the iPhone at \(state.phoneBuiltAt.formatted(date: .omitted, time: .shortened))") + .font(.caption2) + .foregroundStyle(.orange) + } + } + // Keeps the values clear of the page indicator. + .padding(.trailing, 10) + } + .navigationTitle("Statistics") + } + + /// Same colors as the phone's pie: red low, green in range, yellow high. + private var slices: [(String, Double, Color)] { + [ + ("low", max(stats.pieLow, 0.1), .red), + ("range", max(stats.pieRange, 0.1), .green), + ("high", max(stats.pieHigh, 0.1), .yellow), + ] + } + + private func value(_ title: String, _ value: String) -> some View { + VStack(spacing: 0) { + Text(title) + .foregroundStyle(.secondary) + Text(value.isEmpty ? "—" : value) + } + .font(.caption2) + .lineLimit(1) + .minimumScaleFactor(0.6) + .frame(maxWidth: .infinity) + } +} diff --git a/LoopFollowWatch/WatchSession.swift b/LoopFollowWatch/WatchSession.swift index 9f6e2ffb1..755cca713 100644 --- a/LoopFollowWatch/WatchSession.swift +++ b/LoopFollowWatch/WatchSession.swift @@ -41,6 +41,11 @@ final class WatchSession: NSObject, ObservableObject { session.activate() } + /// Commands go through the phone, or directly when the phone sent what that needs. + var canSendCommands: Bool { + isReachable || credentials?.remote != nil + } + /// True while the phone's latest state is recent enough to rely on. var isPhoneDataFresh: Bool { Date().timeIntervalSince(lastPhoneBuiltAt) < Self.phoneStaleAfter diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index 71b08b6d8..b1b1efb40 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -23,6 +23,7 @@ struct WatchProtocolTests { highMgdl: 180, colorBGText: true, infoRows: [WatchInfoRow(name: "IOB", value: "1,2", level: nil), WatchInfoRow(name: "SAGE", value: "9d 2h", level: .warning)], + stats: WatchStats(lowPercent: "2%", inRangePercent: "80%", highPercent: "18%", avgBG: "8,1", estA1CTitle: "GMI:", estA1C: "6,6", stdDevTitle: "Std Dev:", stdDev: "2,3", pieLow: 2, pieRange: 80, pieHigh: 18), lastLoopAt: t0, device: "Trio", alarm: alarm, diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index fc3a3da95..3268f0ead 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -31,6 +31,8 @@ struct WatchState: Codable, Equatable { var colorBGText: Bool /// The phone's visible info table rows, in its order and with its coloring. var infoRows: [WatchInfoRow] + /// The statistics shown under the phone's chart. + var stats: WatchStats? /// When the loop last reported, nil when unknown. var lastLoopAt: Date? /// "Loop", "Trio" or another uploader name; empty for CGM-only setups. @@ -65,6 +67,21 @@ struct WatchReading: Codable, Equatable { var date: Date { Date(timeIntervalSince1970: t) } } +/// The phone's statistics panel, formatted on the phone. +struct WatchStats: Codable, Equatable { + var lowPercent: String + var inRangePercent: String + var highPercent: String + var avgBG: String + var estA1CTitle: String + var estA1C: String + var stdDevTitle: String + var stdDev: String + var pieLow: Double + var pieRange: Double + var pieHigh: Double +} + struct WatchInfoRow: Codable, Equatable { let name: String let value: String From 93709a1ed8925155d5b6e0cb6d6a03fee60364e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:13:36 +0200 Subject: [PATCH 12/25] Scrub the watch chart with the Digital Crown and pick the time span from the toolbar --- LoopFollowWatch/ChartPage.swift | 64 +++++++++++++++++++++++++++- LoopFollowWatch/WatchChartView.swift | 45 ++++++++++++------- 2 files changed, 91 insertions(+), 18 deletions(-) diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index 59519adc9..d354cdd3a 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -2,16 +2,76 @@ // ChartPage.swift import SwiftUI +import WatchKit -/// The full chart on its own page. +/// The full chart on its own page. Tap the chart to scrub back in time with the +/// Digital Crown; tap again to return to now. The button in the corner changes +/// the time span. struct ChartPage: View { let state: WatchState + @AppStorage("chartSpanHours") private var spanHours: Double = 3 + @State private var isScrubbing = false + @State private var scrubSteps = 0.0 + @FocusState private var crownFocused: Bool + + /// The crown moves in five-minute steps, back to the start of the 24 hours the phone sends. + private var maxSteps: Double { max(0, (24 - spanHours * 0.75) * 12) } + var body: some View { TimelineView(.periodic(from: .now, by: 60)) { context in - WatchChartView(state: state, now: context.date) + VStack(spacing: 2) { + if isScrubbing { + scrubHeader(now: context.date) + } + WatchChartView( + state: state, + now: context.date, + spanHours: spanHours, + scrubOffset: isScrubbing ? scrubSteps * 300 : nil + ) // Keeps the axis labels clear of the page indicator. .padding(.trailing, 10) + .contentShape(Rectangle()) + .onTapGesture { + isScrubbing.toggle() + scrubSteps = 0 + crownFocused = isScrubbing + } + } + .focusable(isScrubbing) + .focused($crownFocused) + .digitalCrownRotation($scrubSteps, from: 0, through: maxSteps, by: 1, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("\(Int(spanHours)) h") { + let index = WatchChartView.spans.firstIndex(of: spanHours) ?? 1 + spanHours = WatchChartView.spans[(index + 1) % WatchChartView.spans.count] + scrubSteps = min(scrubSteps, maxSteps) + } + .accessibilityLabel("Time span \(Int(spanHours)) hours") + } + } + } + + /// Time and BG at the marker. + private func scrubHeader(now: Date) -> some View { + let marker = WatchChartView.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: scrubSteps * 300) + let nearest = state.readings.min { abs($0.date.timeIntervalSince(marker)) < abs($1.date.timeIntervalSince(marker)) } + let reading = nearest.flatMap { abs($0.date.timeIntervalSince(marker)) <= 450 ? $0 : nil } + return HStack { + Text(marker.formatted(date: .omitted, time: .shortened)) + .foregroundStyle(.secondary) + Spacer() + if let reading { + Text(WatchFormat.glucose(reading.v, unit: state.snapshot?.unit ?? .mgdl)) + .foregroundStyle(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) + } else { + Text("—") + } } + .font(.headline) + .monospacedDigit() } } diff --git a/LoopFollowWatch/WatchChartView.swift b/LoopFollowWatch/WatchChartView.swift index 164c2e30a..494f820bc 100644 --- a/LoopFollowWatch/WatchChartView.swift +++ b/LoopFollowWatch/WatchChartView.swift @@ -6,13 +6,14 @@ import SwiftUI /// BG chart in the phone's colors: readings colored by range, dashed low/high /// lines, prediction lines or cone in their phone colors, boluses and carbs as markers. -/// Tap to change the time span. struct WatchChartView: View { let state: WatchState let now: Date + let spanHours: Double + /// How far back the chart is scrolled; nil when not scrubbing. + var scrubOffset: TimeInterval? - private static let spans: [Double] = [1, 3, 6, 12, 24] - @AppStorage("chartSpanHours") private var spanHours: Double = 3 + static let spans: [Double] = [1, 3, 6, 12, 24] private var unit: GlucoseSnapshot.Unit { state.snapshot?.unit ?? .mgdl } @@ -45,6 +46,12 @@ struct WatchChartView: View { .foregroundStyle(Color.blue.opacity(0.4)) } + if let marker = markerDate { + RuleMark(x: .value("Marker", marker)) + .foregroundStyle(Color.white.opacity(0.8)) + .lineStyle(StrokeStyle(lineWidth: 1)) + } + ForEach(predictions, id: \.0) { kind, points in ForEach(points, id: \.t) { point in LineMark( @@ -103,24 +110,30 @@ struct WatchChartView: View { } } } - .overlay(alignment: .topLeading) { - Text("\(Int(spanHours)) h") - .font(.system(size: 10)) - .foregroundStyle(.secondary) - } - .contentShape(Rectangle()) - .onTapGesture { - let index = Self.spans.firstIndex(of: spanHours) ?? 1 - spanHours = Self.spans[(index + 1) % Self.spans.count] - } - .accessibilityLabel("BG chart, last \(Int(spanHours)) hours. Tap to change the time span.") + .accessibilityLabel("BG chart, last \(Int(spanHours)) hours.") } - /// The visible span ends a little after now so the prediction stays visible. private var visibleWindow: ClosedRange { + Self.window(state: state, now: now, spanHours: spanHours, scrubOffset: scrubOffset ?? 0) + } + + private var markerDate: Date? { + scrubOffset.map { Self.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: $0) } + } + + /// The visible span ends a little after now so the prediction stays visible; + /// while scrubbing it moves back by the scroll offset. + static func window(state: WatchState, now: Date, spanHours: Double, scrubOffset: TimeInterval) -> ClosedRange { let span = spanHours * 3600 let lookAhead = state.predictions.isEmpty && state.cone.isEmpty ? span * 0.05 : span * 0.25 - return now.addingTimeInterval(-span + lookAhead) ... now.addingTimeInterval(lookAhead) + let end = now.addingTimeInterval(lookAhead - scrubOffset) + return end.addingTimeInterval(-span) ... end + } + + /// While scrubbing, the time the marker points at: three quarters across the chart. + static func markerDate(state: WatchState, now: Date, spanHours: Double, scrubOffset: TimeInterval) -> Date { + window(state: state, now: now, spanHours: spanHours, scrubOffset: scrubOffset).upperBound + .addingTimeInterval(-spanHours * 3600 * 0.25) } private func yTop(readings: [WatchReading], predictions: [WatchReading]) -> Double { From 488db47b86f174dee58c7dc68c8f85dbe018528e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:15:46 +0200 Subject: [PATCH 13/25] Test watch command validation, signing key choice, devicestatus parsing and Loop override payloads --- LoopFollowWatch/StandaloneFetcher.swift | 95 +--------------- RemoteShared/DeviceStatusSummary.swift | 92 ++++++++++++++++ Tests/WatchRemoteTests.swift | 138 ++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 90 deletions(-) create mode 100644 RemoteShared/DeviceStatusSummary.swift create mode 100644 Tests/WatchRemoteTests.swift diff --git a/LoopFollowWatch/StandaloneFetcher.swift b/LoopFollowWatch/StandaloneFetcher.swift index 8b36dcc96..550937d24 100644 --- a/LoopFollowWatch/StandaloneFetcher.swift +++ b/LoopFollowWatch/StandaloneFetcher.swift @@ -20,8 +20,6 @@ enum StandaloneFetcher { } private static let history: TimeInterval = 24 * 3600 - /// How far ahead predictions are drawn. - private static let predictionPoints = 36 static func fetch(credentials: WatchCredentials, base: WatchState?, now: Date = Date()) async throws -> WatchState { if !credentials.nightscoutURL.isEmpty { @@ -44,7 +42,7 @@ enum StandaloneFetcher { ]) async let deviceStatus = get(credentials, "/api/v1/devicestatus.json", ["count": "1"]) async let treatments = get(credentials, "/api/v1/treatments.json", [ - "find[created_at][$gte]": iso.string(from: since), + "find[created_at][$gte]": DeviceStatusSummary.iso.string(from: since), "count": "500", ]) @@ -54,7 +52,7 @@ enum StandaloneFetcher { let status = (try? JSONSerialization.jsonObject(with: await deviceStatus) as? [[String: Any]])?.first let treatmentList = (try? JSONSerialization.jsonObject(with: await treatments) as? [[String: Any]]) ?? [] - return build(glucose: glucose, status: status.map { parseDeviceStatus($0, base: base) }, treatments: treatmentList, base: base, now: now) + return build(glucose: glucose, status: status.map { DeviceStatusSummary.parse($0, showsCone: base?.cone.isEmpty == false) }, treatments: treatmentList, base: base, now: now) } private static func get(_ credentials: WatchCredentials, _ path: String, _ parameters: [String: String]) async throws -> Data { @@ -92,16 +90,7 @@ enum StandaloneFetcher { // MARK: - Building - struct LoopStatus { - var loopAt: Date? - var iob: Double? - var cob: Double? - var recommendedBolus: Double? - var predictions: [WatchPredictionLine] = [] - var cone: [WatchConePoint] = [] - } - - private static func build(glucose: [ShareGlucoseData], status: LoopStatus?, treatments: [[String: Any]], base: WatchState?, now: Date) -> WatchState { + private static func build(glucose: [ShareGlucoseData], status: DeviceStatusSummary?, treatments: [[String: Any]], base: WatchState?, now: Date) -> WatchState { let cutoff = now.timeIntervalSince1970 - history let readings = glucose.filter { $0.date >= cutoff }.map { WatchReading(t: $0.date, v: Double($0.sgv)) } let latest = glucose.last @@ -155,65 +144,9 @@ enum StandaloneFetcher { ) } - /// Reads the loop's latest report the same way the phone does: `loop` for Loop, `openaps` for Trio. - static func parseDeviceStatus(_ status: [String: Any], base: WatchState?) -> LoopStatus { - var result = LoopStatus() - if let loop = status["loop"] as? [String: Any] { - result.loopAt = date(loop["timestamp"]) - result.iob = (loop["iob"] as? [String: Any])?["iob"] as? Double - result.cob = (loop["cob"] as? [String: Any])?["cob"] as? Double - result.recommendedBolus = loop["recommendedBolus"] as? Double - if let predicted = loop["predicted"] as? [String: Any], - let values = predicted["values"] as? [Double], - let start = date(predicted["startDate"]) - { - result.predictions = [WatchPredictionLine(kind: .loop, points: points(values, start: start))] - } - } else if let openaps = status["openaps"] as? [String: Any] { - let suggested = openaps["suggested"] as? [String: Any] ?? openaps["enacted"] as? [String: Any] ?? [:] - result.loopAt = date(suggested["deliverAt"] ?? suggested["timestamp"]) - let iob = openaps["iob"] as? [String: Any] ?? (openaps["iob"] as? [[String: Any]])?.first - result.iob = iob?["iob"] as? Double ?? suggested["IOB"] as? Double - result.cob = suggested["COB"] as? Double - result.recommendedBolus = openaps["recommendedBolus"] as? Double - if let predBGs = suggested["predBGs"] as? [String: [Double]], let start = result.loopAt { - let lines: [(WatchPredictionLine.Kind, String)] = [(.zt, "ZT"), (.iob, "IOB"), (.cob, "COB"), (.uam, "UAM")] - let series = lines.compactMap { kind, key in predBGs[key].map { (kind, points($0, start: start)) } } - // The phone shows Trio's forecast as a cone or as lines; keep what it showed last. - if base?.cone.isEmpty == false { - result.cone = cone(series.map(\.1)) - } else { - result.predictions = series.map { WatchPredictionLine(kind: $0.0, points: $0.1) } - } - } - } - return result - } - - private static func points(_ values: [Double], start: Date) -> [WatchReading] { - values.prefix(predictionPoints + 1).enumerated().map { index, value in - WatchReading(t: start.timeIntervalSince1970 + Double(index) * 300, v: value) - } - } - - /// Same construction as the phone: the shortest series sets the length, min and max per step. - private static func cone(_ series: [[WatchReading]]) -> [WatchConePoint] { - guard let length = series.map(\.count).min(), length > 0 else { return [] } - return (0 ..< length).map { index in - let values = series.map { $0[index].v } - var low = values.min() ?? 0 - var high = values.max() ?? 0 - if low == high { - low -= 1 - high += 1 - } - return WatchConePoint(t: series[0][index].t, low: low, high: high) - } - } - private static func reading(_ treatment: [String: Any], key: String, cutoff: TimeInterval) -> WatchReading? { guard let value = treatment[key] as? Double, value > 0, - let at = date(treatment["timestamp"] ?? treatment["created_at"]), + let at = DeviceStatusSummary.date(treatment["timestamp"] ?? treatment["created_at"]), at.timeIntervalSince1970 >= cutoff else { return nil } return WatchReading(t: at.timeIntervalSince1970, v: value) @@ -224,7 +157,7 @@ enum StandaloneFetcher { func latest(_ eventType: String) -> (start: Date, entry: [String: Any])? { treatments .filter { $0["eventType"] as? String == eventType } - .compactMap { entry in date(entry["timestamp"] ?? entry["created_at"]).map { ($0, entry) } } + .compactMap { entry in DeviceStatusSummary.date(entry["timestamp"] ?? entry["created_at"]).map { ($0, entry) } } .max { $0.0 < $1.0 } } @@ -246,22 +179,4 @@ enum StandaloneFetcher { } return (override, tempTarget) } - - // MARK: - Dates - - private static let iso: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formatter - }() - - private static let isoNoFraction = ISO8601DateFormatter() - - private static func date(_ value: Any?) -> Date? { - if let number = value as? Double { - return Date(timeIntervalSince1970: number > 1e11 ? number / 1000 : number) - } - guard let string = value as? String else { return nil } - return iso.date(from: string) ?? isoNoFraction.date(from: string) - } } diff --git a/RemoteShared/DeviceStatusSummary.swift b/RemoteShared/DeviceStatusSummary.swift new file mode 100644 index 000000000..99216bba3 --- /dev/null +++ b/RemoteShared/DeviceStatusSummary.swift @@ -0,0 +1,92 @@ +// LoopFollow +// DeviceStatusSummary.swift + +import Foundation + +/// The parts of the latest Nightscout devicestatus the watch shows when it +/// fetches on its own. +struct DeviceStatusSummary: Equatable { + var loopAt: Date? + var iob: Double? + var cob: Double? + var recommendedBolus: Double? + var predictions: [WatchPredictionLine] = [] + var cone: [WatchConePoint] = [] + + /// How far ahead predictions are drawn. + static let predictionPoints = 36 + + /// Reads a devicestatus record the way the phone does: `loop` for Loop, `openaps` for Trio. + static func parse(_ status: [String: Any], showsCone: Bool) -> DeviceStatusSummary { + var result = DeviceStatusSummary() + if let loop = status["loop"] as? [String: Any] { + result.loopAt = date(loop["timestamp"]) + result.iob = (loop["iob"] as? [String: Any])?["iob"] as? Double + result.cob = (loop["cob"] as? [String: Any])?["cob"] as? Double + result.recommendedBolus = loop["recommendedBolus"] as? Double + if let predicted = loop["predicted"] as? [String: Any], + let values = predicted["values"] as? [Double], + let start = date(predicted["startDate"]) + { + result.predictions = [WatchPredictionLine(kind: .loop, points: points(values, start: start))] + } + } else if let openaps = status["openaps"] as? [String: Any] { + let suggested = openaps["suggested"] as? [String: Any] ?? openaps["enacted"] as? [String: Any] ?? [:] + result.loopAt = date(suggested["deliverAt"] ?? suggested["timestamp"]) + let iob = openaps["iob"] as? [String: Any] ?? (openaps["iob"] as? [[String: Any]])?.first + result.iob = iob?["iob"] as? Double ?? suggested["IOB"] as? Double + result.cob = suggested["COB"] as? Double + result.recommendedBolus = openaps["recommendedBolus"] as? Double + if let predBGs = suggested["predBGs"] as? [String: [Double]], let start = result.loopAt { + let lines: [(WatchPredictionLine.Kind, String)] = [(.zt, "ZT"), (.iob, "IOB"), (.cob, "COB"), (.uam, "UAM")] + let series = lines.compactMap { kind, key in predBGs[key].map { (kind, points($0, start: start)) } } + // The phone shows Trio's forecast as a cone or as lines; keep what it showed last. + if showsCone { + result.cone = cone(series.map(\.1)) + } else { + result.predictions = series.map { WatchPredictionLine(kind: $0.0, points: $0.1) } + } + } + } + return result + } + + private static func points(_ values: [Double], start: Date) -> [WatchReading] { + values.prefix(predictionPoints + 1).enumerated().map { index, value in + WatchReading(t: start.timeIntervalSince1970 + Double(index) * 300, v: value) + } + } + + /// Same construction as the phone: the shortest series sets the length, min and max per step. + private static func cone(_ series: [[WatchReading]]) -> [WatchConePoint] { + guard let length = series.map(\.count).min(), length > 0 else { return [] } + return (0 ..< length).map { index in + let values = series.map { $0[index].v } + var low = values.min() ?? 0 + var high = values.max() ?? 0 + if low == high { + low -= 1 + high += 1 + } + return WatchConePoint(t: series[0][index].t, low: low, high: high) + } + } + + // MARK: - Dates + + static let iso: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + }() + + private static let isoNoFraction = ISO8601DateFormatter() + + static func date(_ value: Any?) -> Date? { + if let number = value as? Double { + return Date(timeIntervalSince1970: number > 1e11 ? number / 1000 : number) + } + guard let string = value as? String else { return nil } + return iso.date(from: string) ?? isoNoFraction.date(from: string) + } +} diff --git a/Tests/WatchRemoteTests.swift b/Tests/WatchRemoteTests.swift new file mode 100644 index 000000000..55c0c5a22 --- /dev/null +++ b/Tests/WatchRemoteTests.swift @@ -0,0 +1,138 @@ +// LoopFollow +// WatchRemoteTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +struct WatchRemoteTests { + private let now = Date(timeIntervalSince1970: 1_790_000_000) + + private let limits = WatchCommandValidator.Limits( + maxBolus: 2, + bolusIncrement: 0.05, + maxCarbs: 60, + maxFat: 30, + maxProtein: 30, + mealWithBolus: true, + mealWithFatProtein: false + ) + + private func validate(_ action: WatchCommand.Action, _ kind: WatchRemoteConfig.Kind, limits: WatchCommandValidator.Limits? = nil) -> Result { + WatchCommandValidator.validate(action, kind: kind, limits: limits ?? self.limits, now: now) + } + + // MARK: - Validation + + @Test("Trio boluses round down to the increment and respect the maximum") + func trioBolus() { + #expect(validate(.bolus(units: 1.27), .trio) == .success(.bolus(units: 1.25))) + #expect((try? validate(.bolus(units: 2.5), .trio).get()) == nil) + #expect((try? validate(.bolus(units: 0.01), .trio).get()) == nil) + } + + @Test("Loop boluses are sent as entered, up to the maximum") + func loopBolus() { + #expect(validate(.bolus(units: 1.27), .loop) == .success(.bolus(units: 1.27))) + #expect((try? validate(.bolus(units: 2.01), .loop).get()) == nil) + } + + @Test("Loop meals are carbs only, within the time window Loop accepts") + func loopMeal() { + #expect((try? validate(.meal(carbs: 30, fat: 0, protein: 0, bolus: 0, consumedAt: now), .loop).get()) != nil) + #expect((try? validate(.meal(carbs: 30, fat: 0, protein: 0, bolus: 1, consumedAt: now), .loop).get()) == nil) + #expect((try? validate(.meal(carbs: 70, fat: 0, protein: 0, bolus: 0, consumedAt: now), .loop).get()) == nil) + #expect((try? validate(.meal(carbs: 30, fat: 0, protein: 0, bolus: 0, consumedAt: now.addingTimeInterval(-13 * 3600)), .loop).get()) == nil) + #expect((try? validate(.meal(carbs: 30, fat: 0, protein: 0, bolus: 0, consumedAt: now.addingTimeInterval(2 * 3600)), .loop).get()) == nil) + } + + @Test("Trio meals follow the phone's Meal settings") + func trioMeal() { + #expect((try? validate(.meal(carbs: 30, fat: 10, protein: 0, bolus: 0, consumedAt: now), .trio).get()) == nil) + #expect(validate(.meal(carbs: 30, fat: 0, protein: 0, bolus: 1.27, consumedAt: now), .trio) + == .success(.meal(carbs: 30, fat: 0, protein: 0, bolus: 1.25, consumedAt: now))) + var noBolus = limits + noBolus.mealWithBolus = false + #expect((try? validate(.meal(carbs: 30, fat: 0, protein: 0, bolus: 1, consumedAt: now), .trio, limits: noBolus).get()) == nil) + } + + @Test("Temp targets are refused for Loop") + func loopTempTarget() { + #expect((try? validate(.startTempTarget(presetID: "x"), .loop).get()) == nil) + #expect((try? validate(.startTempTarget(presetID: "x"), .trio).get()) != nil) + } + + // MARK: - Signing key + + @Test("LoopFollow's own key signs when both apps share a team or no remote team is set") + func signingKey() { + let lf = (key: "LF", keyID: "LFKEY") + let remote = (key: "R", keyID: "RKEY") + #expect(RemoteSigningKey.resolve(lfTeamID: "T1", remoteTeamID: "T1", lfKey: lf, remoteKey: remote).key == "LF") + #expect(RemoteSigningKey.resolve(lfTeamID: "T1", remoteTeamID: "", lfKey: lf, remoteKey: remote).key == "LF") + let other = RemoteSigningKey.resolve(lfTeamID: "T1", remoteTeamID: "T2", lfKey: lf, remoteKey: remote) + #expect(other.key == "R") + #expect(other.teamID == "T2") + } + + // MARK: - Device status + + @Test("Loop devicestatus gives IOB, COB, recommendation and the prediction") + func loopDeviceStatus() { + let status: [String: Any] = [ + "loop": [ + "timestamp": "2026-10-19T08:00:00Z", + "iob": ["iob": 1.2], + "cob": ["cob": 15.0], + "recommendedBolus": 0.3, + "predicted": ["startDate": "2026-10-19T08:00:00Z", "values": [120.0, 125.0, 130.0]], + ], + ] + let summary = DeviceStatusSummary.parse(status, showsCone: false) + #expect(summary.iob == 1.2) + #expect(summary.cob == 15) + #expect(summary.recommendedBolus == 0.3) + #expect(summary.predictions.first?.kind == .loop) + #expect(summary.predictions.first?.points.map(\.v) == [120, 125, 130]) + #expect(summary.loopAt == DeviceStatusSummary.date("2026-10-19T08:00:00.000Z")) + } + + @Test("Trio devicestatus gives lines or the cone, matching what the phone showed") + func trioDeviceStatus() { + let status: [String: Any] = [ + "openaps": [ + "iob": ["iob": 1.5], + "suggested": [ + "timestamp": "2026-10-19T08:00:00.000Z", + "COB": 20.0, + "predBGs": ["IOB": [140.0, 138.0], "ZT": [140.0, 130.0], "COB": [140.0, 150.0]], + ], + ], + ] + let lines = DeviceStatusSummary.parse(status, showsCone: false) + #expect(lines.iob == 1.5) + #expect(lines.cob == 20) + #expect(Set(lines.predictions.map(\.kind)) == [.iob, .zt, .cob]) + + let cone = DeviceStatusSummary.parse(status, showsCone: true) + #expect(cone.predictions.isEmpty) + #expect(cone.cone.map(\.low) == [139, 130]) + #expect(cone.cone.map(\.high) == [141, 150]) + } + + // MARK: - Loop override payload + + @Test("Loop override payload carries name, duration and return info") + func loopOverridePayload() { + let info = ReturnNotificationInfo(productionEnvironment: false, deviceToken: "tok", bundleId: "b", teamId: "t", keyId: "k", apnsKey: "a") + let payload = LoopAPNSCommands.overridePayload(presetName: "Running", duration: 3600, returnInfo: info, now: now) + #expect(payload["override-name"] as? String == "Running") + #expect(payload["override-duration-minutes"] as? Int == 60) + #expect(payload["alert"] as? String == "Running Temporary Override (1h 0m)") + #expect((payload["return_notification"] as? [String: Any])?["device_token"] as? String == "tok") + + let cancel = LoopAPNSCommands.cancelOverridePayload(returnInfo: nil, now: now) + #expect(cancel["cancel-temporary-override"] as? String == "true") + #expect(cancel["return_notification"] == nil) + } +} From 93f31dc0e19909fa47d33677f479007a9c1518ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:24:28 +0200 Subject: [PATCH 14/25] Schedule the watch background refresh once the app is running --- LoopFollowWatch/LoopFollowWatchApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LoopFollowWatch/LoopFollowWatchApp.swift b/LoopFollowWatch/LoopFollowWatchApp.swift index 24d7deaf0..8db456fa3 100644 --- a/LoopFollowWatch/LoopFollowWatchApp.swift +++ b/LoopFollowWatch/LoopFollowWatchApp.swift @@ -10,7 +10,6 @@ struct LoopFollowWatchApp: App { init() { WatchSession.shared.activate() - WatchSession.shared.scheduleBackgroundRefresh() } var body: some Scene { @@ -20,6 +19,7 @@ struct LoopFollowWatchApp: App { .task(id: scenePhase) { guard scenePhase == .active else { return } session.requestState() + session.scheduleBackgroundRefresh() // While open, check every minute whether the watch has to fetch on its own. while !Task.isCancelled { await session.refreshIfNeeded() From 58ca297ccef3fb4e3486d5fbccce8726a96ed9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:43:09 +0200 Subject: [PATCH 15/25] Deliver credentials to the watch by message as well as by transfer, and send directly when the phone is unreachable before delivery --- LoopFollow/Watch/WatchSessionManager.swift | 8 ++- LoopFollowWatch/WatchKeychain.swift | 7 ++- LoopFollowWatch/WatchSession.swift | 58 +++++++++++++++++----- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/LoopFollow/Watch/WatchSessionManager.swift b/LoopFollow/Watch/WatchSessionManager.swift index 6e0dcb4ab..08a9aacb1 100644 --- a/LoopFollow/Watch/WatchSessionManager.swift +++ b/LoopFollow/Watch/WatchSessionManager.swift @@ -99,6 +99,10 @@ final class WatchSessionManager: NSObject { for transfer in session.outstandingUserInfoTransfers where transfer.userInfo[WatchCredentials.userInfoKey] != nil { transfer.cancel() } + // Immediate when the watch app is reachable; the queued transfer covers the rest. + if session.isReachable { + session.sendMessage([WatchCredentials.userInfoKey: data], replyHandler: nil, errorHandler: nil) + } session.transferUserInfo([WatchCredentials.userInfoKey: data]) lastSentCredentials = credentials LogManager.shared.log(category: .general, message: "[Watch] credentials sent") @@ -168,8 +172,8 @@ extension WatchSessionManager: WCSessionDelegate { func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { DispatchQueue.main.async { if message[WatchCredentials.requestKey] != nil { - self.sendCredentialsIfChanged(force: true) - replyHandler([:]) + let data = (try? JSONEncoder().encode(WatchCredentialsBuilder.build())) ?? Data() + replyHandler([WatchCredentials.userInfoKey: data]) return } diff --git a/LoopFollowWatch/WatchKeychain.swift b/LoopFollowWatch/WatchKeychain.swift index f5db58dbc..3c53feb87 100644 --- a/LoopFollowWatch/WatchKeychain.swift +++ b/LoopFollowWatch/WatchKeychain.swift @@ -24,9 +24,12 @@ enum WatchKeychain { kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, ] - let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) + var status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) if status == errSecItemNotFound { - SecItemAdd(query.merging(attributes) { $1 } as CFDictionary, nil) + status = SecItemAdd(query.merging(attributes) { $1 } as CFDictionary, nil) + } + if status != errSecSuccess { + LogManager.shared.log(category: .general, message: "Keychain save failed: \(status)") } } diff --git a/LoopFollowWatch/WatchSession.swift b/LoopFollowWatch/WatchSession.swift index 755cca713..4bc317f4d 100644 --- a/LoopFollowWatch/WatchSession.swift +++ b/LoopFollowWatch/WatchSession.swift @@ -69,9 +69,11 @@ final class WatchSession: NSObject, ObservableObject { try? await Task.sleep(nanoseconds: 8_000_000_000) guard !isPhoneDataFresh else { return } } - guard let credentials, credentials.hasDataSource, - Date().timeIntervalSince(lastStandaloneAttempt) >= Self.standaloneInterval - else { return } + guard let credentials, credentials.hasDataSource else { + LogManager.shared.log(category: .general, message: "Phone data is stale, but the watch has no data source credentials yet") + return + } + guard Date().timeIntervalSince(lastStandaloneAttempt) >= Self.standaloneInterval else { return } lastStandaloneAttempt = Date() do { let fetched = try await StandaloneFetcher.fetch(credentials: credentials, base: state) @@ -84,10 +86,15 @@ final class WatchSession: NSObject, ObservableObject { } /// Sends a command through the phone when it is reachable, otherwise directly. - /// Never both: a command that fails through the phone is not retried directly. + /// Falls back to sending directly only when the phone turned out to be out of + /// reach before the command was delivered; a command that may have reached + /// the phone is never sent a second time. func send(_ action: WatchCommand.Action) async -> WatchCommandReply { if session.activationState == .activated, session.isReachable { - return await sendThroughPhone(action) + switch await sendThroughPhone(action) { + case let .replied(reply): return reply + case .notDelivered: break + } } if case .snoozeAlarm = action { return WatchCommandReply(success: false, message: "Snoozing needs the iPhone, which is not reachable right now.") @@ -98,20 +105,30 @@ final class WatchSession: NSObject, ObservableObject { return await DirectCommandSender.send(action, credentials: credentials, config: config) } - private func sendThroughPhone(_ action: WatchCommand.Action) async -> WatchCommandReply { + private enum PhoneSendResult { + case replied(WatchCommandReply) + /// WatchConnectivity reported the phone unreachable, so the command never left the watch. + case notDelivered + } + + private func sendThroughPhone(_ action: WatchCommand.Action) async -> PhoneSendResult { guard let data = try? JSONEncoder().encode(WatchCommand(action: action)) else { - return WatchCommandReply(success: false, message: "Could not encode the command.") + return .replied(WatchCommandReply(success: false, message: "Could not encode the command.")) } return await withCheckedContinuation { continuation in session.sendMessage([WatchCommand.messageKey: data], replyHandler: { reply in let result = (reply[WatchCommandReply.messageKey] as? Data) .flatMap { try? JSONDecoder().decode(WatchCommandReply.self, from: $0) } - continuation.resume(returning: result ?? WatchCommandReply(success: false, message: "Unexpected reply from the iPhone.")) + continuation.resume(returning: .replied(result ?? WatchCommandReply(success: false, message: "Unexpected reply from the iPhone."))) }, errorHandler: { error in - continuation.resume(returning: WatchCommandReply( + if (error as? WCError)?.code == .notReachable { + continuation.resume(returning: .notDelivered) + return + } + continuation.resume(returning: .replied(WatchCommandReply( success: false, message: "No reply from the iPhone (\(error.localizedDescription)). Check LoopFollow on the iPhone before trying again." - )) + ))) }) } } @@ -163,16 +180,23 @@ final class WatchSession: NSObject, ObservableObject { } private func receiveCredentials(_ data: Data) { - guard let received = try? JSONDecoder().decode(WatchCredentials.self, from: data) else { return } + guard let received = try? JSONDecoder().decode(WatchCredentials.self, from: data) else { + LogManager.shared.log(category: .general, message: "Could not decode credentials from the phone") + return + } credentials = received WatchKeychain.save(received) + LogManager.shared.log(category: .general, message: "Credentials received (remote commands: \(received.remote != nil))") } /// Asks the phone for credentials when the watch has none, e.g. after a reinstall. private func requestCredentialsIfMissing() { guard credentials == nil, session.activationState == .activated else { return } if session.isReachable { - session.sendMessage([WatchCredentials.requestKey: true], replyHandler: nil, errorHandler: nil) + session.sendMessage([WatchCredentials.requestKey: true], replyHandler: { reply in + guard let data = reply[WatchCredentials.userInfoKey] as? Data else { return } + Task { @MainActor in self.receiveCredentials(data) } + }, errorHandler: nil) } else { session.transferUserInfo([WatchCredentials.requestKey: true]) } @@ -195,7 +219,10 @@ extension WatchSession: WCSessionDelegate { let reachable = session.isReachable Task { @MainActor in self.isReachable = reachable - if reachable { self.requestState() } + if reachable { + self.requestState() + self.requestCredentialsIfMissing() + } } } @@ -204,6 +231,11 @@ extension WatchSession: WCSessionDelegate { Task { @MainActor in self.applyFromPhone(data) } } + nonisolated func session(_: WCSession, didReceiveMessage message: [String: Any]) { + guard let credentials = message[WatchCredentials.userInfoKey] as? Data else { return } + Task { @MainActor in self.receiveCredentials(credentials) } + } + nonisolated func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { let state = userInfo[WatchState.contextKey] as? Data let credentials = userInfo[WatchCredentials.userInfoKey] as? Data From d7b3ad6ee810a8d112bc2c553f20dce25961113f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 15:57:28 +0200 Subject: [PATCH 16/25] Convert Nightscout dates on the watch and give up waiting for the phone after 30 seconds --- LoopFollowWatch/StandaloneFetcher.swift | 2 ++ LoopFollowWatch/WatchSession.swift | 40 +++++++++++++++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/LoopFollowWatch/StandaloneFetcher.swift b/LoopFollowWatch/StandaloneFetcher.swift index 550937d24..71f838e85 100644 --- a/LoopFollowWatch/StandaloneFetcher.swift +++ b/LoopFollowWatch/StandaloneFetcher.swift @@ -46,8 +46,10 @@ enum StandaloneFetcher { "count": "500", ]) + // Nightscout dates are milliseconds; the rest of the app works in seconds, as on the phone. let glucose = try JSONDecoder().decode([ShareGlucoseData].self, from: await entries) .filter { $0.sgv > 0 } + .map { ShareGlucoseData(sgv: $0.sgv, date: $0.date / 1000, direction: $0.direction) } .sorted { $0.date < $1.date } let status = (try? JSONSerialization.jsonObject(with: await deviceStatus) as? [[String: Any]])?.first let treatmentList = (try? JSONSerialization.jsonObject(with: await treatments) as? [[String: Any]]) ?? [] diff --git a/LoopFollowWatch/WatchSession.swift b/LoopFollowWatch/WatchSession.swift index 4bc317f4d..c6101921b 100644 --- a/LoopFollowWatch/WatchSession.swift +++ b/LoopFollowWatch/WatchSession.swift @@ -115,24 +115,28 @@ final class WatchSession: NSObject, ObservableObject { guard let data = try? JSONEncoder().encode(WatchCommand(action: action)) else { return .replied(WatchCommandReply(success: false, message: "Could not encode the command.")) } + let noReply = WatchCommandReply( + success: false, + message: "No reply from the iPhone. The command may still have been sent: check Loop or Trio before trying again." + ) return await withCheckedContinuation { continuation in + let once = ResumeOnce(continuation) session.sendMessage([WatchCommand.messageKey: data], replyHandler: { reply in let result = (reply[WatchCommandReply.messageKey] as? Data) .flatMap { try? JSONDecoder().decode(WatchCommandReply.self, from: $0) } - continuation.resume(returning: .replied(result ?? WatchCommandReply(success: false, message: "Unexpected reply from the iPhone."))) + once.resume(.replied(result ?? WatchCommandReply(success: false, message: "Unexpected reply from the iPhone."))) }, errorHandler: { error in - if (error as? WCError)?.code == .notReachable { - continuation.resume(returning: .notDelivered) - return - } - continuation.resume(returning: .replied(WatchCommandReply( - success: false, - message: "No reply from the iPhone (\(error.localizedDescription)). Check LoopFollow on the iPhone before trying again." - ))) + once.resume((error as? WCError)?.code == .notReachable ? .notDelivered : .replied(noReply)) }) + DispatchQueue.main.asyncAfter(deadline: .now() + Self.phoneReplyTimeout) { + once.resume(.replied(noReply)) + } } } + /// Longest wait for the phone's verdict before telling the user it is unknown. + private static let phoneReplyTimeout: TimeInterval = 30 + /// Lets a WatchConnectivity background task finish once queued data has been delivered. func waitForPendingContent() async { activate() @@ -245,3 +249,21 @@ extension WatchSession: WCSessionDelegate { } } } + +/// Resumes a continuation at most once, from whichever callback comes first. +private final class ResumeOnce: @unchecked Sendable { + private var continuation: CheckedContinuation? + private let lock = NSLock() + + init(_ continuation: CheckedContinuation) { + self.continuation = continuation + } + + func resume(_ value: T) { + lock.lock() + let pending = continuation + continuation = nil + lock.unlock() + pending?.resume(returning: value) + } +} From 0f6a56ce291fe4548e2ae7397d0cf026080559aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 Oct 2026 16:26:44 +0200 Subject: [PATCH 17/25] Leave out watch chart hour labels too close to the edges to fit --- LoopFollowWatch/WatchChartView.swift | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/LoopFollowWatch/WatchChartView.swift b/LoopFollowWatch/WatchChartView.swift index 494f820bc..b6be4c090 100644 --- a/LoopFollowWatch/WatchChartView.swift +++ b/LoopFollowWatch/WatchChartView.swift @@ -95,7 +95,7 @@ struct WatchChartView: View { .chartXScale(domain: window.lowerBound ... window.upperBound) .chartYScale(domain: 39 ... top) .chartXAxis { - AxisMarks(values: .stride(by: .hour, count: spanHours > 6 ? 6 : (spanHours > 3 ? 2 : 1))) { _ in + AxisMarks(values: hourTicks(in: window)) { _ in AxisGridLine().foregroundStyle(Color.gray.opacity(0.3)) AxisValueLabel(format: .dateTime.hour()) } @@ -136,6 +136,26 @@ struct WatchChartView: View { .addingTimeInterval(-spanHours * 3600 * 0.25) } + /// Whole hours in the window, leaving out those too close to an edge for their label to fit. + private func hourTicks(in window: ClosedRange) -> [Date] { + let step = spanHours > 6 ? 6 : (spanHours > 3 ? 2 : 1) + let margin = (window.upperBound.timeIntervalSince(window.lowerBound)) * 0.06 + let calendar = Calendar.current + guard var tick = calendar.dateInterval(of: .hour, for: window.lowerBound)?.end else { return [] } + var ticks: [Date] = [] + while tick < window.upperBound { + let hour = calendar.component(.hour, from: tick) + if hour % step == 0, + tick.timeIntervalSince(window.lowerBound) > margin, + window.upperBound.timeIntervalSince(tick) > margin + { + ticks.append(tick) + } + tick = tick.addingTimeInterval(3600) + } + return ticks + } + private func yTop(readings: [WatchReading], predictions: [WatchReading]) -> Double { let highest = max(readings.map(\.v).max() ?? 0, predictions.map(\.v).max() ?? 0) return min(max(250, highest + 20), 400) From c7908d923f406cdfa5f4f5059c57134fd2401b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 20 Oct 2026 20:12:27 +0200 Subject: [PATCH 18/25] Show only the reading age on the watch glucose page, keep the toolbar buttons the same size, and let Trio meals carry a bolus as on the phone The crown symbol for setting an amount follows the wrist the watch is worn on. --- LoopFollow/Watch/WatchStateBuilder.swift | 3 -- LoopFollowWatch/CrownSymbol.swift | 11 +++++ LoopFollowWatch/GlucosePage.swift | 28 +++++------ LoopFollowWatch/Remote/BolusEntryView.swift | 2 +- .../Remote/CommandConfirmView.swift | 2 +- LoopFollowWatch/Remote/MealEntryView.swift | 46 +++++++++---------- LoopFollowWatch/StandaloneFetcher.swift | 1 - Tests/WatchProtocolTests.swift | 1 - WatchShared/WatchState.swift | 2 - 9 files changed, 45 insertions(+), 51 deletions(-) create mode 100644 LoopFollowWatch/CrownSymbol.swift diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index 74a35a94a..eaf59df86 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -45,8 +45,6 @@ enum WatchStateBuilder { WatchConePoint(t: $0.date.timeIntervalSince1970, low: $0.yMin, high: $0.yMax) } - let lastLoop = Observable.shared.alertLastLoopTime.value.flatMap { $0 > 0 ? Date(timeIntervalSince1970: $0) : nil } - return WatchState( builtAt: now, source: .phone, @@ -62,7 +60,6 @@ enum WatchStateBuilder { colorBGText: Storage.shared.colorBGText.value, infoRows: infoRows(vc), stats: vc.map { stats($0.statsDisplayModel) }, - lastLoopAt: lastLoop, device: Storage.shared.device.value, alarm: currentAlarm(), remote: remoteConfig() diff --git a/LoopFollowWatch/CrownSymbol.swift b/LoopFollowWatch/CrownSymbol.swift new file mode 100644 index 000000000..1b6d66f23 --- /dev/null +++ b/LoopFollowWatch/CrownSymbol.swift @@ -0,0 +1,11 @@ +// LoopFollow +// CrownSymbol.swift + +import WatchKit + +enum CrownSymbol { + /// The crown symbol turning the way that raises a value, for the wrist the watch is worn on. + static var increase: String { + WKInterfaceDevice.current().wristLocation == .left ? "digitalcrown.arrow.clockwise" : "digitalcrown.arrow.counterclockwise" + } +} diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index ae944dd27..ed5ba466a 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -103,20 +103,11 @@ struct GlucosePage: View { Label("Not looping", systemImage: "exclamationmark.triangle.fill") .font(.footnote) .foregroundStyle(.red) - } else if snapshot.cob != nil || state.lastLoopAt != nil { - HStack { - if let cob = snapshot.cob { - Text("COB \(WatchFormat.cob(cob))") - } - Spacer(minLength: 4) - if let lastLoop = state.lastLoopAt { - Text("Loop \(WatchFormat.minutesAgo(lastLoop))") - } - } - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) - .minimumScaleFactor(0.7) + } else if let cob = snapshot.cob { + Text("COB \(WatchFormat.cob(cob))") + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) } } } @@ -144,8 +135,6 @@ private struct RemoteToolbar: View { } label: { Image(systemName: "plus") } - .controlSize(.large) - .buttonStyle(.borderedProminent) .tint(.blue) .accessibilityLabel("Meal or bolus") .disabled(!session.canSendCommands) @@ -160,7 +149,12 @@ private struct RemoteToolbar: View { .accessibilityLabel("Temp Target") .disabled(!session.canSendCommands) } else { - Spacer() + // Holds the trailing slot so + stays centered. + Button {} label: { + Image(systemName: "scope") + } + .hidden() + .accessibilityHidden(true) } } } diff --git a/LoopFollowWatch/Remote/BolusEntryView.swift b/LoopFollowWatch/Remote/BolusEntryView.swift index 39a218c55..108c9305d 100644 --- a/LoopFollowWatch/Remote/BolusEntryView.swift +++ b/LoopFollowWatch/Remote/BolusEntryView.swift @@ -30,7 +30,7 @@ struct BolusPickerView: View { Button { navigator.push(.bolusAmount) } label: { - Label("Other Amount", systemImage: "digitalcrown.horizontal.arrow.clockwise") + Label("Other Amount", systemImage: CrownSymbol.increase) } } .navigationTitle("Bolus") diff --git a/LoopFollowWatch/Remote/CommandConfirmView.swift b/LoopFollowWatch/Remote/CommandConfirmView.swift index dcfb58ec2..d7e8cbbb7 100644 --- a/LoopFollowWatch/Remote/CommandConfirmView.swift +++ b/LoopFollowWatch/Remote/CommandConfirmView.swift @@ -94,7 +94,7 @@ struct CommandConfirmView: View { .trim(from: 0, to: crownProgress) .stroke(Color.blue, style: StrokeStyle(lineWidth: 6, lineCap: .round)) .rotationEffect(.degrees(-90)) - Image(systemName: "digitalcrown.arrow.clockwise") + Image(systemName: CrownSymbol.increase) .font(.title3) } .frame(width: 56, height: 56) diff --git a/LoopFollowWatch/Remote/MealEntryView.swift b/LoopFollowWatch/Remote/MealEntryView.swift index ec2d1fea8..364cfdda6 100644 --- a/LoopFollowWatch/Remote/MealEntryView.swift +++ b/LoopFollowWatch/Remote/MealEntryView.swift @@ -3,8 +3,8 @@ import SwiftUI -/// Carbs first; fat, protein, bolus (Trio, per the phone's Meal settings) and -/// time behind "More". Everything goes to the phone as one command. +/// Carbs, and for Trio fat, protein and a bolus per the phone's Meal settings, +/// as on the phone; the time behind a button. Everything goes as one command. struct MealEntryView: View { let remote: WatchRemoteConfig @@ -14,7 +14,7 @@ struct MealEntryView: View { @State private var protein = 0.0 @State private var bolus = 0.0 @State private var offsetMinutes = 0.0 - @State private var showMore = false + @State private var showTime = false @FocusState private var focus: Field? private enum Field: Hashable { @@ -32,28 +32,28 @@ struct MealEntryView: View { AmountField(value: $carbs, unit: "g", step: 1, maximum: remote.maxCarbs, isHighlighted: focus == .carbs, format: grams) } - if showMore { - if remote.mealWithFatProtein { - field("Fat", .fat) { - AmountField(value: $fat, unit: "g", step: 1, maximum: remote.maxFat, isHighlighted: focus == .fat, format: grams) - } - field("Protein", .protein) { - AmountField(value: $protein, unit: "g", step: 1, maximum: remote.maxProtein, isHighlighted: focus == .protein, format: grams) - } + if remote.mealWithFatProtein { + field("Fat", .fat) { + AmountField(value: $fat, unit: "g", step: 1, maximum: remote.maxFat, isHighlighted: focus == .fat, format: grams) } - if remote.mealWithBolus { - field("Bolus", .bolus) { - AmountField(value: $bolus, unit: "U", step: bolusStep, maximum: remote.maxBolus, isHighlighted: focus == .bolus, format: WatchFormat.insulin) - } + field("Protein", .protein) { + AmountField(value: $protein, unit: "g", step: 1, maximum: remote.maxProtein, isHighlighted: focus == .protein, format: grams) } + } + if remote.mealWithBolus { + field("Bolus", .bolus) { + AmountField(value: $bolus, unit: "U", step: bolusStep, maximum: remote.maxBolus, isHighlighted: focus == .bolus, format: WatchFormat.insulin) + } + } + if showTime { field("Time", .time) { AmountField(value: $offsetMinutes, unit: "min", step: 5, minimum: -120, maximum: 60, isHighlighted: focus == .time, format: timeText) } } else { Button { - showMore = true + showTime = true } label: { - Label(moreTitle, systemImage: "ellipsis.circle") + Label("Time", systemImage: "clock") .font(.footnote) .frame(maxWidth: .infinity, alignment: .leading) } @@ -105,14 +105,6 @@ struct MealEntryView: View { } } - private var moreTitle: String { - var parts: [String] = [] - if remote.mealWithFatProtein { parts.append("Fat, Protein") } - if remote.mealWithBolus { parts.append("Bolus") } - parts.append("Time") - return parts.joined(separator: ", ") - } - private var logTitle: String { if bolus > 0 { return "Log \(Int(carbs)) g + \(WatchFormat.insulin(bolus)) U" } return "Log \(Int(carbs)) g" @@ -143,6 +135,10 @@ struct MealEntryView: View { if bolus > 0 { lines.append("Bolus \(WatchFormat.insulin(bolus)) U") } let at = Date().addingTimeInterval(offsetMinutes * 60) lines.append(offsetMinutes == 0 ? "Now" : at.formatted(date: .omitted, time: .shortened)) + if bolus > 0, offsetMinutes != 0 { + // Same note as the phone's meal screen. + lines.append("The meal will be scheduled, but the bolus is enacted immediately.") + } return CommandRequest( title: "Meal", lines: lines, diff --git a/LoopFollowWatch/StandaloneFetcher.swift b/LoopFollowWatch/StandaloneFetcher.swift index 71f838e85..ea13ad588 100644 --- a/LoopFollowWatch/StandaloneFetcher.swift +++ b/LoopFollowWatch/StandaloneFetcher.swift @@ -139,7 +139,6 @@ enum StandaloneFetcher { colorBGText: base?.colorBGText ?? true, infoRows: base?.infoRows ?? [], stats: base?.stats, - lastLoopAt: status?.loopAt, device: base?.device ?? "", alarm: nil, remote: remote diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index b1b1efb40..1854e27b8 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -24,7 +24,6 @@ struct WatchProtocolTests { colorBGText: true, infoRows: [WatchInfoRow(name: "IOB", value: "1,2", level: nil), WatchInfoRow(name: "SAGE", value: "9d 2h", level: .warning)], stats: WatchStats(lowPercent: "2%", inRangePercent: "80%", highPercent: "18%", avgBG: "8,1", estA1CTitle: "GMI:", estA1C: "6,6", stdDevTitle: "Std Dev:", stdDev: "2,3", pieLow: 2, pieRange: 80, pieHigh: 18), - lastLoopAt: t0, device: "Trio", alarm: alarm, remote: WatchRemoteConfig( diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index 3268f0ead..095784b9f 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -33,8 +33,6 @@ struct WatchState: Codable, Equatable { var infoRows: [WatchInfoRow] /// The statistics shown under the phone's chart. var stats: WatchStats? - /// When the loop last reported, nil when unknown. - var lastLoopAt: Date? /// "Loop", "Trio" or another uploader name; empty for CGM-only setups. var device: String /// The alarm currently sounding on the phone. From 119dd142b56473f6efb25ca1f05e4f236d81c173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 21 Oct 2026 07:45:58 +0200 Subject: [PATCH 19/25] Let the crown scroll the watch meal screen until a field is tapped --- LoopFollowWatch/Remote/MealEntryView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LoopFollowWatch/Remote/MealEntryView.swift b/LoopFollowWatch/Remote/MealEntryView.swift index 364cfdda6..2c809d606 100644 --- a/LoopFollowWatch/Remote/MealEntryView.swift +++ b/LoopFollowWatch/Remote/MealEntryView.swift @@ -5,6 +5,7 @@ import SwiftUI /// Carbs, and for Trio fat, protein and a bolus per the phone's Meal settings, /// as on the phone; the time behind a button. Everything goes as one command. +/// The crown scrolls the page until a field is tapped. struct MealEntryView: View { let remote: WatchRemoteConfig @@ -52,6 +53,7 @@ struct MealEntryView: View { } else { Button { showTime = true + focus = .time } label: { Label("Time", systemImage: "clock") .font(.footnote) @@ -72,7 +74,6 @@ struct MealEntryView: View { } } .navigationTitle("Meal") - .onAppear { focus = .carbs } } /// The phone's Quick-Pick meals, limited to what the phone's Meal settings allow. @@ -102,6 +103,7 @@ struct MealEntryView: View { .foregroundStyle(.secondary) content() .focused($focus, equals: id) + .onTapGesture { focus = id } } } From 6a84c2ff943796a54ad46bdc68d9505ead16c46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 21 Oct 2026 08:09:02 +0200 Subject: [PATCH 20/25] Show Other Amount on the watch bolus list as plain text --- LoopFollowWatch/Remote/BolusEntryView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LoopFollowWatch/Remote/BolusEntryView.swift b/LoopFollowWatch/Remote/BolusEntryView.swift index 108c9305d..5e4064340 100644 --- a/LoopFollowWatch/Remote/BolusEntryView.swift +++ b/LoopFollowWatch/Remote/BolusEntryView.swift @@ -30,7 +30,7 @@ struct BolusPickerView: View { Button { navigator.push(.bolusAmount) } label: { - Label("Other Amount", systemImage: CrownSymbol.increase) + Text("Other Amount") } } .navigationTitle("Bolus") From ce6aab8c675eb8bc76ef55fc60cb243e1b9f5161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 21 Oct 2026 09:01:47 +0200 Subject: [PATCH 21/25] Fit the watch statistics page on one screen on every watch size and wrap the waiting-for-iPhone title --- LoopFollowWatch/ContentView.swift | 1 + LoopFollowWatch/StatsPage.swift | 55 ++++++++++++++++--------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/LoopFollowWatch/ContentView.swift b/LoopFollowWatch/ContentView.swift index 95d14d8c0..24e470ca9 100644 --- a/LoopFollowWatch/ContentView.swift +++ b/LoopFollowWatch/ContentView.swift @@ -96,6 +96,7 @@ private struct WaitingForPhoneView: View { Text("Waiting for LoopFollow on iPhone") .font(.headline) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) Text("Open LoopFollow on the paired iPhone.") .font(.footnote) .foregroundStyle(.secondary) diff --git a/LoopFollowWatch/StatsPage.swift b/LoopFollowWatch/StatsPage.swift index 04ec64221..aa9057e2c 100644 --- a/LoopFollowWatch/StatsPage.swift +++ b/LoopFollowWatch/StatsPage.swift @@ -10,38 +10,39 @@ struct StatsPage: View { let stats: WatchStats var body: some View { - ScrollView { - VStack(spacing: 8) { - Chart(slices, id: \.0) { slice in - SectorMark(angle: .value("share", slice.1)) - .foregroundStyle(slice.2) - } - .chartLegend(.hidden) - .frame(height: 70) - .accessibilityHidden(true) + VStack(spacing: 8) { + Chart(slices, id: \.0) { slice in + SectorMark(angle: .value("share", slice.1)) + .foregroundStyle(slice.2) + } + .chartLegend(.hidden) + // Shrinks so the page fits the smaller watch sizes without scrolling. + .frame(minHeight: 30, maxHeight: 70) + .accessibilityHidden(true) - Grid(horizontalSpacing: 4, verticalSpacing: 6) { - GridRow { - value("Low:", stats.lowPercent) - value("In Range:", stats.inRangePercent) - value("High:", stats.highPercent) - } - GridRow { - value("Avg BG:", stats.avgBG) - value(stats.estA1CTitle, stats.estA1C) - value(stats.stdDevTitle, stats.stdDev) - } + Grid(horizontalSpacing: 4, verticalSpacing: 6) { + GridRow { + value("Low:", stats.lowPercent) + value("In Range:", stats.inRangePercent) + value("High:", stats.highPercent) } - - if state.source == .watch { - Text("From the iPhone at \(state.phoneBuiltAt.formatted(date: .omitted, time: .shortened))") - .font(.caption2) - .foregroundStyle(.orange) + GridRow { + value("Avg BG:", stats.avgBG) + value(stats.estA1CTitle, stats.estA1C) + value(stats.stdDevTitle, stats.stdDev) } } - // Keeps the values clear of the page indicator. - .padding(.trailing, 10) + .layoutPriority(1) + + if state.source == .watch { + Text("From the iPhone at \(state.phoneBuiltAt.formatted(date: .omitted, time: .shortened))") + .font(.caption2) + .foregroundStyle(.orange) + .layoutPriority(1) + } } + // Keeps the values clear of the page indicator. + .padding(.trailing, 10) .navigationTitle("Statistics") } From 657000a90b07caf713e26ef5d607e3f103752fb5 Mon Sep 17 00:00:00 2001 From: Phil A <76601115+MtlPhil@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:21:10 -0400 Subject: [PATCH 22/25] Watch: centered glucose page, working chart scrubbing, and overrides (#789) * Center the watch BG with IOB and COB icons, and fix crown scrubbing on the chart page The trend arrow no longer shifts the BG digits or the delta off center, and IOB and COB share one line under the delta with syringe and fork-and-knife icons. On the chart page, crown focus is now requested after the page becomes focusable, so the crown scrubs instead of paging. A crown button in the toolbar shows the feature and toggles it. * Show BG, carbs and insulin under the watch chart while scrubbing The readout moves below the chart so it no longer collides with the toolbar buttons, and shows a range-colored dot with the BG plus any carbs and boluses that fall within the marker's five-minute crown step. * Show overrides on the watch: a band on the chart and the running one on the glucose page The phone now sends the override periods of the last 24 hours with their name and insulin needs, taking the percentage from the matching preset when Nightscout does not carry it. The chart draws them as a thin green band along the top, and the glucose page shows the running override's name and percentage under IOB and COB. * Show the override's name and percentage under the watch chart The line shows the running override, or the one at the marker while scrubbing, and shares its view with the glucose page. --- LoopFollow/Watch/WatchStateBuilder.swift | 19 +++++ LoopFollowWatch/ChartPage.swift | 92 ++++++++++++++++++------ LoopFollowWatch/GlucosePage.swift | 80 ++++++++++++++------- LoopFollowWatch/WatchChartView.swift | 15 +++- Tests/WatchProtocolTests.swift | 13 ++++ WatchShared/WatchState.swift | 22 ++++++ 6 files changed, 195 insertions(+), 46 deletions(-) diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index eaf59df86..21d89021c 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -27,6 +27,10 @@ enum WatchStateBuilder { .filter { $0.date >= cutoff } .map { WatchReading(t: $0.date, v: $0.value) } + let overrides = (vc?.overrideGraphData ?? []) + .filter { $0.endDate >= cutoff } + .map { WatchOverride(start: $0.date, end: $0.endDate, name: $0.reason.isEmpty ? "Override" : $0.reason, percent: overridePercent($0)) } + var predictions: [WatchPredictionLine] = [] if let vc { let lines: [(WatchPredictionLine.Kind, [ShareGlucoseData])] = [ @@ -55,6 +59,7 @@ enum WatchStateBuilder { cone: cone, boluses: boluses, carbs: carbs, + overrides: overrides, lowMgdl: thresholds.low, highMgdl: thresholds.high, colorBGText: Storage.shared.colorBGText.value, @@ -167,6 +172,20 @@ enum WatchStateBuilder { ) } + /// Insulin needs of an override, from its Nightscout entry or else from the preset of the same name, + /// since Trio does not upload the factor with the override. + private static func overridePercent(_ override: DataStructs.overrideStruct) -> Int? { + var factor = override.insulNeedsScaleFactor + if factor == 1 { + if let loop = ProfileManager.shared.loopOverrides.first(where: { $0.name == override.reason }) { + factor = loop.insulinNeedsScaleFactor + } else if let percentage = ProfileManager.shared.trioOverrides.first(where: { $0.name == override.reason })?.percentage { + factor = percentage / 100 + } + } + return factor == 1 ? nil : Int(round(factor * 100)) + } + private static func loopOverridePresets() -> [WatchPreset] { ProfileManager.shared.loopOverrides.map { override in var parts: [String] = [] diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index d354cdd3a..9af911995 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -4,9 +4,9 @@ import SwiftUI import WatchKit -/// The full chart on its own page. Tap the chart to scrub back in time with the -/// Digital Crown; tap again to return to now. The button in the corner changes -/// the time span. +/// The full chart on its own page. Tap the chart or the crown button to scrub back +/// in time with the Digital Crown; tap again to return to now. The button in the +/// other corner changes the time span. The override line under the chart follows the marker. struct ChartPage: View { let state: WatchState @@ -21,9 +21,6 @@ struct ChartPage: View { var body: some View { TimelineView(.periodic(from: .now, by: 60)) { context in VStack(spacing: 2) { - if isScrubbing { - scrubHeader(now: context.date) - } WatchChartView( state: state, now: context.date, @@ -33,17 +30,38 @@ struct ChartPage: View { // Keeps the axis labels clear of the page indicator. .padding(.trailing, 10) .contentShape(Rectangle()) - .onTapGesture { - isScrubbing.toggle() - scrubSteps = 0 - crownFocused = isScrubbing + .onTapGesture(perform: toggleScrubbing) + // Below the chart, clear of the toolbar buttons that float over the top of the page. + if isScrubbing { + scrubDetails(now: context.date) + } + if let override = state.activeOverride(at: isScrubbing ? markerDate(now: context.date) : context.date) { + OverrideLine(name: override.name, percent: override.percent) + .padding(.trailing, 10) } } .focusable(isScrubbing) .focused($crownFocused) + // Focus can only move once the page has become focusable, so it follows the toggle rather than + // being set in the same tap; otherwise the crown keeps paging instead of scrubbing. + .onChange(of: isScrubbing) { _, scrubbing in + crownFocused = scrubbing + } + .onChange(of: crownFocused) { _, focused in + if !focused { + isScrubbing = false + } + } .digitalCrownRotation($scrubSteps, from: 0, through: maxSteps, by: 1, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) } .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: toggleScrubbing) { + Image(systemName: CrownSymbol.increase) + } + .tint(isScrubbing ? .green : nil) + .accessibilityLabel(isScrubbing ? "Stop scrubbing" : "Scrub with the Digital Crown") + } ToolbarItem(placement: .topBarTrailing) { Button("\(Int(spanHours)) h") { let index = WatchChartView.spans.firstIndex(of: spanHours) ?? 1 @@ -55,23 +73,55 @@ struct ChartPage: View { } } - /// Time and BG at the marker. - private func scrubHeader(now: Date) -> some View { - let marker = WatchChartView.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: scrubSteps * 300) + private func toggleScrubbing() { + isScrubbing.toggle() + scrubSteps = 0 + } + + /// Time, BG, carbs and insulin at the marker. Carbs and boluses count when they fall within the + /// marker's five-minute crown step, so each entry shows at exactly one step. + private func scrubDetails(now: Date) -> some View { + let marker = markerDate(now: now) let nearest = state.readings.min { abs($0.date.timeIntervalSince(marker)) < abs($1.date.timeIntervalSince(marker)) } let reading = nearest.flatMap { abs($0.date.timeIntervalSince(marker)) <= 450 ? $0 : nil } - return HStack { + let carbs = total(of: state.carbs, near: marker) + let insulin = total(of: state.boluses, near: marker) + return HStack(spacing: 8) { Text(marker.formatted(date: .omitted, time: .shortened)) .foregroundStyle(.secondary) - Spacer() - if let reading { - Text(WatchFormat.glucose(reading.v, unit: state.snapshot?.unit ?? .mgdl)) - .foregroundStyle(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) - } else { - Text("—") + HStack(spacing: 3) { + if let reading { + Circle() + .fill(WatchFormat.bgColor(reading.v, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl)) + .frame(width: 7, height: 7) + Text(WatchFormat.glucose(reading.v, unit: state.snapshot?.unit ?? .mgdl)) + } else { + Text("—") + } + } + if let carbs { + Text("\(Image(systemName: "fork.knife")) \(WatchFormat.cob(carbs))") + .accessibilityLabel("Carbs \(WatchFormat.cob(carbs))") + } + if let insulin { + Text("\(Image(systemName: "syringe")) \(WatchFormat.insulin(insulin)) U") + .accessibilityLabel("Bolus \(WatchFormat.insulin(insulin)) U") } } - .font(.headline) + .font(.footnote) .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.6) + .padding(.trailing, 10) + } + + private func markerDate(now: Date) -> Date { + WatchChartView.markerDate(state: state, now: now, spanHours: spanHours, scrubOffset: scrubSteps * 300) + } + + /// Sum of the entries within half a crown step of the marker, or nil when there are none. + private func total(of entries: [WatchReading], near marker: Date) -> Double? { + let matching = entries.filter { abs($0.date.timeIntervalSince(marker)) < 150 } + return matching.isEmpty ? nil : matching.reduce(0) { $0 + $1.v } } } diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index ed5ba466a..d07086dd2 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -4,7 +4,7 @@ import Charts import SwiftUI -/// Main page: one large BG value with trend and delta, a two-hour sparkline, +/// Main page: one large BG value with trend and delta, IOB and COB, the running override, a two-hour sparkline, /// and the remote commands in the bottom toolbar. struct GlucosePage: View { let state: WatchState @@ -21,19 +21,12 @@ struct GlucosePage: View { glucose(now: context.date) if !isLuminanceReduced { details + overrideLine(now: context.date) Sparkline(state: state, now: context.date) .frame(minHeight: 36) } } .toolbar { - ToolbarItem(placement: .topBarLeading) { - if !isLuminanceReduced, let iob = state.snapshot?.iob { - Text("IOB \(WatchFormat.iob(iob))") - .font(.footnote) - .lineLimit(1) - .minimumScaleFactor(0.6) - } - } ToolbarItem(placement: .topBarTrailing) { age(now: context.date) } @@ -51,16 +44,20 @@ struct GlucosePage: View { if let snapshot = state.snapshot { let isStale = now.timeIntervalSince(snapshot.updatedAt) >= WatchFormat.staleAfter VStack(spacing: -4) { - HStack(alignment: .center, spacing: 4) { - Text(WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit)) - .font(.system(size: 56, weight: .black)) - .foregroundStyle(WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText)) - .strikethrough(isStale, pattern: .solid, color: .red) - .lineLimit(1) - .minimumScaleFactor(0.5) - Text(WatchFormat.trendArrow(snapshot.trend)) - .font(.system(size: 28, weight: .black)) - } + // The arrow hangs off the number's trailing edge so the digits, and the delta under them, stay centered. + Text(WatchFormat.glucose(snapshot.glucose, unit: snapshot.unit)) + .font(.system(size: 56, weight: .black)) + .foregroundStyle(WatchFormat.bgColor(snapshot.glucose, lowMgdl: state.lowMgdl, highMgdl: state.highMgdl, colored: state.colorBGText)) + .strikethrough(isStale, pattern: .solid, color: .red) + .lineLimit(1) + .minimumScaleFactor(0.5) + .overlay(alignment: .trailing) { + Text(WatchFormat.trendArrow(snapshot.trend)) + .font(.system(size: 28, weight: .black)) + .fixedSize() + .alignmentGuide(.trailing) { $0[.leading] - 4 } + } + .padding(.horizontal, 34) Text(WatchFormat.delta(snapshot.delta, unit: snapshot.unit)) .font(.title3) .foregroundStyle(.secondary) @@ -103,14 +100,49 @@ struct GlucosePage: View { Label("Not looping", systemImage: "exclamationmark.triangle.fill") .font(.footnote) .foregroundStyle(.red) - } else if let cob = snapshot.cob { - Text("COB \(WatchFormat.cob(cob))") - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) + } else if snapshot.iob != nil || snapshot.cob != nil { + HStack(spacing: 12) { + if let iob = snapshot.iob { + Text("\(Image(systemName: "syringe")) \(WatchFormat.iob(iob))") + .accessibilityLabel("Insulin on board \(WatchFormat.iob(iob))") + } + if let cob = snapshot.cob { + Text("\(Image(systemName: "fork.knife")) \(WatchFormat.cob(cob))") + .accessibilityLabel("Carbs on board \(WatchFormat.cob(cob))") + } + } + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.7) } } } + + /// The running override's name and insulin needs. + @ViewBuilder + private func overrideLine(now: Date) -> some View { + let active = state.activeOverride(at: now) + // The phone's periods end on time; the watch's own fetch only has the snapshot's name. + if let name = state.overrides == nil ? state.snapshot?.override : active?.name { + OverrideLine(name: name, percent: active?.percent) + } + } +} + +/// An override's name and insulin needs, in the green of the overrides button and chart band. +struct OverrideLine: View { + let name: String + let percent: Int? + + var body: some View { + Text("\(Image(systemName: "slider.horizontal.3")) \(name)\(percent.map { " · \($0) %" } ?? "")") + .font(.footnote) + .foregroundStyle(.green) + .lineLimit(1) + .minimumScaleFactor(0.7) + .accessibilityLabel("Override \(name)\(percent.map { ", insulin needs \($0) percent" } ?? "")") + } } /// Override, treatments and temp target, as in Apple's bottom-toolbar pattern. diff --git a/LoopFollowWatch/WatchChartView.swift b/LoopFollowWatch/WatchChartView.swift index b6be4c090..71cc3225f 100644 --- a/LoopFollowWatch/WatchChartView.swift +++ b/LoopFollowWatch/WatchChartView.swift @@ -5,7 +5,8 @@ import Charts import SwiftUI /// BG chart in the phone's colors: readings colored by range, dashed low/high -/// lines, prediction lines or cone in their phone colors, boluses and carbs as markers. +/// lines, prediction lines or cone in their phone colors, boluses and carbs as markers, +/// and overrides as a green band along the top. struct WatchChartView: View { let state: WatchState let now: Date @@ -24,6 +25,7 @@ struct WatchChartView: View { (line.kind, line.points.filter { window.contains($0.date) }) } let cone = state.cone.filter { window.contains($0.date) } + let overrides = (state.overrides ?? []).filter { $0.endDate > window.lowerBound && $0.startDate < window.upperBound } let top = yTop(readings: readings, predictions: predictions.flatMap(\.1) + cone.map { WatchReading(t: $0.t, v: $0.high) }) Chart { @@ -46,6 +48,17 @@ struct WatchChartView: View { .foregroundStyle(Color.blue.opacity(0.4)) } + // A thin band along the top edge, like the override strip on the phone's chart. + ForEach(overrides, id: \.start) { override in + RectangleMark( + xStart: .value("Start", max(override.startDate, window.lowerBound)), + xEnd: .value("End", min(override.endDate, window.upperBound)), + y: .value("Override", top), + height: .fixed(4) + ) + .foregroundStyle(Color.green.opacity(0.8)) + } + if let marker = markerDate { RuleMark(x: .value("Marker", marker)) .foregroundStyle(Color.white.opacity(0.8)) diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index 1854e27b8..ebb5231b0 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -19,6 +19,10 @@ struct WatchProtocolTests { cone: [WatchConePoint(t: t0.timeIntervalSince1970 + 300, low: 120, high: 150)], boluses: [WatchReading(t: t0.timeIntervalSince1970 - 600, v: 2.5)], carbs: [WatchReading(t: t0.timeIntervalSince1970 - 600, v: 30)], + overrides: [ + WatchOverride(start: t0.timeIntervalSince1970 - 7200, end: t0.timeIntervalSince1970 - 3600, name: "Sleep", percent: nil), + WatchOverride(start: t0.timeIntervalSince1970 - 1800, end: t0.timeIntervalSince1970 + 1800, name: "Exercise", percent: 80), + ], lowMgdl: 70, highMgdl: 180, colorBGText: true, @@ -52,6 +56,15 @@ struct WatchProtocolTests { #expect(WatchState.decode(data) == original) } + @Test("The active override is the one whose period contains the time") + func activeOverride() { + let state = state() + #expect(state.activeOverride(at: t0)?.name == "Exercise") + #expect(state.activeOverride(at: t0.addingTimeInterval(-5400))?.name == "Sleep") + #expect(state.activeOverride(at: t0.addingTimeInterval(-3000)) == nil) + #expect(state.activeOverride(at: t0.addingTimeInterval(1800)) == nil) + } + @Test("Commands keep their action, id and creation time") func commandRoundTrip() throws { let actions: [WatchCommand.Action] = [ diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index 095784b9f..91c73c69c 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -24,6 +24,8 @@ struct WatchState: Codable, Equatable { var cone: [WatchConePoint] var boluses: [WatchReading] var carbs: [WatchReading] + /// Override periods in the last 24 hours, oldest first; nil from the watch's own fetch. + var overrides: [WatchOverride]? /// Range thresholds used for coloring, in mg/dL (same as the phone). var lowMgdl: Double var highMgdl: Double @@ -65,6 +67,26 @@ struct WatchReading: Codable, Equatable { var date: Date { Date(timeIntervalSince1970: t) } } +/// One override period, as drawn in the band at the top of the phone's chart. +struct WatchOverride: Codable, Equatable { + /// Unix epoch seconds. + let start: TimeInterval + let end: TimeInterval + let name: String + /// Insulin needs as a percentage of normal; nil when unchanged. + let percent: Int? + + var startDate: Date { Date(timeIntervalSince1970: start) } + var endDate: Date { Date(timeIntervalSince1970: end) } +} + +extension WatchState { + /// The override running at `date`, if any. + func activeOverride(at date: Date) -> WatchOverride? { + overrides?.last { $0.startDate <= date && date < $0.endDate } + } +} + /// The phone's statistics panel, formatted on the phone. struct WatchStats: Codable, Equatable { var lowPercent: String From 081b7ce8e7a7f6bf252021a45eb51bb3bfbc79cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 23 Oct 2026 19:34:02 +0200 Subject: [PATCH 23/25] Give the crown back to page navigation when watch chart scrubbing ends --- LoopFollowWatch/ChartPage.swift | 44 ++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index 9af911995..588e89938 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -13,7 +13,6 @@ struct ChartPage: View { @AppStorage("chartSpanHours") private var spanHours: Double = 3 @State private var isScrubbing = false @State private var scrubSteps = 0.0 - @FocusState private var crownFocused: Bool /// The crown moves in five-minute steps, back to the start of the 24 hours the phone sends. private var maxSteps: Double { max(0, (24 - spanHours * 0.75) * 12) } @@ -40,19 +39,9 @@ struct ChartPage: View { .padding(.trailing, 10) } } - .focusable(isScrubbing) - .focused($crownFocused) - // Focus can only move once the page has become focusable, so it follows the toggle rather than - // being set in the same tap; otherwise the crown keeps paging instead of scrubbing. - .onChange(of: isScrubbing) { _, scrubbing in - crownFocused = scrubbing - } - .onChange(of: crownFocused) { _, focused in - if !focused { - isScrubbing = false - } - } - .digitalCrownRotation($scrubSteps, from: 0, through: maxSteps, by: 1, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) + .modifier(ScrubCrown(isActive: isScrubbing, steps: $scrubSteps, maxSteps: maxSteps) { + isScrubbing = false + }) } .toolbar { ToolbarItem(placement: .topBarLeading) { @@ -125,3 +114,30 @@ struct ChartPage: View { return matching.isEmpty ? nil : matching.reduce(0) { $0 + $1.v } } } + +/// While scrubbing, the page takes the Digital Crown focus and the crown moves the marker. Otherwise the +/// page has no focus and no crown handling at all, so the crown pages between the watch pages. +private struct ScrubCrown: ViewModifier { + let isActive: Bool + @Binding var steps: Double + let maxSteps: Double + let onFocusLost: () -> Void + + @FocusState private var focused: Bool + + func body(content: Content) -> some View { + if isActive { + content + .focusable() + .focused($focused) + .digitalCrownRotation($steps, from: 0, through: maxSteps, by: 1, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) + // Focus can only move once the view is focusable, so it is requested after it appears. + .task { focused = true } + .onChange(of: focused) { _, isFocused in + if !isFocused { onFocusLost() } + } + } else { + content + } + } +} From c7e7f64d7d100a627a6850f65c51a0102284719a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 23 Oct 2026 20:04:32 +0200 Subject: [PATCH 24/25] Scrub the watch chart on a screen of its own so the crown pages again after going back --- LoopFollowWatch/ChartPage.swift | 122 +++++++++++++++--------------- LoopFollowWatch/ContentView.swift | 7 +- 2 files changed, 67 insertions(+), 62 deletions(-) diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index 588e89938..9beb469b2 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -4,67 +4,82 @@ import SwiftUI import WatchKit -/// The full chart on its own page. Tap the chart or the crown button to scrub back -/// in time with the Digital Crown; tap again to return to now. The button in the -/// other corner changes the time span. The override line under the chart follows the marker. +/// The full chart on its own page. Tap the chart or the crown button to open it for scrubbing +/// with the Digital Crown. The button in the other corner changes the time span. struct ChartPage: View { let state: WatchState + @EnvironmentObject private var navigator: WatchNavigator @AppStorage("chartSpanHours") private var spanHours: Double = 3 - @State private var isScrubbing = false - @State private var scrubSteps = 0.0 - - /// The crown moves in five-minute steps, back to the start of the 24 hours the phone sends. - private var maxSteps: Double { max(0, (24 - spanHours * 0.75) * 12) } var body: some View { TimelineView(.periodic(from: .now, by: 60)) { context in VStack(spacing: 2) { - WatchChartView( - state: state, - now: context.date, - spanHours: spanHours, - scrubOffset: isScrubbing ? scrubSteps * 300 : nil - ) - // Keeps the axis labels clear of the page indicator. - .padding(.trailing, 10) - .contentShape(Rectangle()) - .onTapGesture(perform: toggleScrubbing) - // Below the chart, clear of the toolbar buttons that float over the top of the page. - if isScrubbing { - scrubDetails(now: context.date) - } - if let override = state.activeOverride(at: isScrubbing ? markerDate(now: context.date) : context.date) { + WatchChartView(state: state, now: context.date, spanHours: spanHours, scrubOffset: nil) + // Keeps the axis labels clear of the page indicator. + .padding(.trailing, 10) + .contentShape(Rectangle()) + .onTapGesture { navigator.push(.chartScrub) } + if let override = state.activeOverride(at: context.date) { OverrideLine(name: override.name, percent: override.percent) .padding(.trailing, 10) } } - .modifier(ScrubCrown(isActive: isScrubbing, steps: $scrubSteps, maxSteps: maxSteps) { - isScrubbing = false - }) } .toolbar { ToolbarItem(placement: .topBarLeading) { - Button(action: toggleScrubbing) { + Button { + navigator.push(.chartScrub) + } label: { Image(systemName: CrownSymbol.increase) } - .tint(isScrubbing ? .green : nil) - .accessibilityLabel(isScrubbing ? "Stop scrubbing" : "Scrub with the Digital Crown") + .accessibilityLabel("Scrub with the Digital Crown") } ToolbarItem(placement: .topBarTrailing) { - Button("\(Int(spanHours)) h") { - let index = WatchChartView.spans.firstIndex(of: spanHours) ?? 1 - spanHours = WatchChartView.spans[(index + 1) % WatchChartView.spans.count] - scrubSteps = min(scrubSteps, maxSteps) - } - .accessibilityLabel("Time span \(Int(spanHours)) hours") + SpanButton(spanHours: $spanHours) } } } +} + +/// The chart on a screen of its own, where the Digital Crown moves the marker back in time. +/// The time, BG, carbs, insulin and override at the marker show below the chart. Tap the chart +/// or go back to leave; the crown then pages between the watch pages again. +struct ChartScrubView: View { + let state: WatchState + + @Environment(\.dismiss) private var dismiss + @AppStorage("chartSpanHours") private var spanHours: Double = 3 + @State private var scrubSteps = 0.0 + @FocusState private var crownFocused: Bool + + /// The crown moves in five-minute steps, back to the start of the 24 hours the phone sends. + private var maxSteps: Double { max(0, (24 - spanHours * 0.75) * 12) } - private func toggleScrubbing() { - isScrubbing.toggle() - scrubSteps = 0 + var body: some View { + TimelineView(.periodic(from: .now, by: 60)) { context in + VStack(spacing: 2) { + WatchChartView(state: state, now: context.date, spanHours: spanHours, scrubOffset: scrubSteps * 300) + .contentShape(Rectangle()) + .onTapGesture { dismiss() } + scrubDetails(now: context.date) + if let override = state.activeOverride(at: markerDate(now: context.date)) { + OverrideLine(name: override.name, percent: override.percent) + } + } + .focusable() + .focused($crownFocused) + .digitalCrownRotation($scrubSteps, from: 0, through: maxSteps, by: 1, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) + } + .onAppear { crownFocused = true } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + SpanButton(spanHours: $spanHours) + } + } + .onChange(of: spanHours) { + scrubSteps = min(scrubSteps, maxSteps) + } } /// Time, BG, carbs and insulin at the marker. Carbs and boluses count when they fall within the @@ -101,7 +116,6 @@ struct ChartPage: View { .monospacedDigit() .lineLimit(1) .minimumScaleFactor(0.6) - .padding(.trailing, 10) } private func markerDate(now: Date) -> Date { @@ -115,29 +129,15 @@ struct ChartPage: View { } } -/// While scrubbing, the page takes the Digital Crown focus and the crown moves the marker. Otherwise the -/// page has no focus and no crown handling at all, so the crown pages between the watch pages. -private struct ScrubCrown: ViewModifier { - let isActive: Bool - @Binding var steps: Double - let maxSteps: Double - let onFocusLost: () -> Void +/// Steps through the chart time spans. +private struct SpanButton: View { + @Binding var spanHours: Double - @FocusState private var focused: Bool - - func body(content: Content) -> some View { - if isActive { - content - .focusable() - .focused($focused) - .digitalCrownRotation($steps, from: 0, through: maxSteps, by: 1, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true) - // Focus can only move once the view is focusable, so it is requested after it appears. - .task { focused = true } - .onChange(of: focused) { _, isFocused in - if !isFocused { onFocusLost() } - } - } else { - content + var body: some View { + Button("\(Int(spanHours)) h") { + let index = WatchChartView.spans.firstIndex(of: spanHours) ?? 1 + spanHours = WatchChartView.spans[(index + 1) % WatchChartView.spans.count] } + .accessibilityLabel("Time span \(Int(spanHours)) hours") } } diff --git a/LoopFollowWatch/ContentView.swift b/LoopFollowWatch/ContentView.swift index 24e470ca9..f8abc2df1 100644 --- a/LoopFollowWatch/ContentView.swift +++ b/LoopFollowWatch/ContentView.swift @@ -5,6 +5,7 @@ import SwiftUI /// Screens pushed on the main navigation stack. enum WatchRoute: Hashable { + case chartScrub case treatments case meal case bolus @@ -63,8 +64,12 @@ struct ContentView: View { @ViewBuilder private func destination(_ route: WatchRoute, state: WatchState) -> some View { - if let remote = state.remote { + if route == .chartScrub { + ChartScrubView(state: state) + } else if let remote = state.remote { switch route { + case .chartScrub: + EmptyView() case .treatments: TreatmentMenuView() case .meal: From 4add7bcfdcb58b355f4bd574e13ba9594fb36070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 23 Oct 2026 20:07:58 +0200 Subject: [PATCH 25/25] Open watch chart scrubbing by tapping the chart --- LoopFollowWatch/ChartPage.swift | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/LoopFollowWatch/ChartPage.swift b/LoopFollowWatch/ChartPage.swift index 9beb469b2..d8f17d7f9 100644 --- a/LoopFollowWatch/ChartPage.swift +++ b/LoopFollowWatch/ChartPage.swift @@ -2,10 +2,9 @@ // ChartPage.swift import SwiftUI -import WatchKit -/// The full chart on its own page. Tap the chart or the crown button to open it for scrubbing -/// with the Digital Crown. The button in the other corner changes the time span. +/// The full chart on its own page. Tap the chart to open it for scrubbing with the Digital Crown. +/// The button in the corner changes the time span. struct ChartPage: View { let state: WatchState @@ -27,14 +26,6 @@ struct ChartPage: View { } } .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - navigator.push(.chartScrub) - } label: { - Image(systemName: CrownSymbol.increase) - } - .accessibilityLabel("Scrub with the Digital Crown") - } ToolbarItem(placement: .topBarTrailing) { SpanButton(spanHours: $spanHours) }