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/21] 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 zUSbaqIJi6kBzEh(#|SpV$vmnyFY|!_^}R3Jo9spfrX`#8{*)60Wz{!TAUMhsO7an~ zI6U%zzZ1h!|M3KUc>P~`%ka91{NKC>FVof#Kko+iI8kvKZ|`*!=O~f#AFql|y$m7{ zwhKhF@_!eIqky6xk{M(9PoC5aBW7aFmc7EPCs#Ay3H{|}aelll*p{8Vm!gFIaIfS3 z`q7QlIA-h3d2nFMO3os?8xs60WZ}B5T+aW19b9YAX1eXq0mMft?{XQnc`e6}c4hV^ zu19{)?Ir)o&#P<&=o`oV8SrO=1bK6-{(QHKncf^sFw$3I?>;h-Y~V6mUdO~;e$^~D z`7`i3&w3Ah5FuW#UKs{L#93u>UTr`@FY$-CoD#{s%Y2D+4GN&HgU8tH&nZ}UkG&b- z)pnA;$8kL4v)wdo=p~yVW*myu4uSvWo_j$&K$o=p6#pRUkG4}roMJ|D7MFRg*uc?; zWge^T_>|<@!jO9dxT*Yi*V}E|%H~Q>ko=zsxFv63msha(x?WN2vu{4*N59v8*Vp{K zO#aK_v=~;rCG%qNR22VY>}nL7fu=?;3GcH1Ev8^XrTtqR>>hs^A0CX7*l8&ZnW|3Z z2z&KmclhI$eO`jF$ZWc_RWxn7ns~)M>3+Um^)AbW0n6BSDP1cTTdBw$f51`r)I<`; z7)21CY}*ur&MSqBWO$cR2g&!Mw$n9s`R4*2_gL-?VPHC% zb%TtaU%>w+OB3Jfy3?0a#?SKm_Xj`x5&MQe^zHV?|L4DIY^H3x zUfTA7r_!j;6j$5(fefq^F|A*@@?s6WC48^rf0uuvnOUPfdA_{2#*+q@+ritD`&C$G z9cHMNBypoQje-ejs&Se4Uj(3631s*$o7xxdNiT)>Nrz3wb0%xi^91P)?LI?sLMNIt zK&?DSqRm&qR|(Nh2rxK~69U%!ccJY|X7Vq0`SyHKe{Aa*Mj8CXl^gKy@MkAV!BhIK zoi|?V1IRVoSu%;w4fvU?CP-pf&g}a|JjRTFAi_JvKW=8*xBb&2{x*ydOD`YMXrs&9 z!K1>TR|90tTw!357uB8t7-PW3a91Q)gb)b>;TwZQcp1^L)3xLg1A?ZqWuCZ{ ztSQ~^m|FB3fExpVQ^vW>NgI7$EKxdj5coGd_?eG}X+Adrd~&hi`&^zAC8?9|yjK{N zkY`#?hj9i+J=xX(k_^R>tnm`lK6o(K>v(in(N@(Z{YkiOEX!s1ng$FGm@m=9$%K>9^Q1ts8 zKqCCp5H)A^%({_Wp8CI=Ylh*8Gf^V$%?0^o3#BR{;`CmQG9-CmA zXOa8JRv`)l($aVF)R6(6fZPG!mQaNm^# z+`!S~P6N1ti~BnV9niifJyG}dSJw$s{x*C!SPBo*aYC(rb8z4(({H!^e>NC6*YgHP zeP6v6Z)~@}?arBT-n0%0Ad_$DPUI^R^#{|F~|DofYIAkJRCk(Ls;21U7583Q3THkoD+({k&F83M${h3zy z8v_aZoh2R6c-hzs3^`;1K_rfPtYlZ^BT=K%4}=!DZ?c_~TqX&nWfRZ|WW~XM_gs>{ z0)pJ|&}&&f@JE23iLK}oeZFykT>}QW)c<7vmKQg}jDPlNDrb`T5&ks@qjrIJ_|K6i zf_dwZ8}@NflgSB$=N}BXaXhfE_N{-icZvRhhj7Ed|FM?+3t#r*?JIxb7uemUlEa?< z?TLTISF|{OFuG{$e8<-j|2!#E4@`t(sHgbqpg`eiV{9;mW8Loy{@kgx=j_`SHEzQ* zidjkk7BSW5cP+Lx@RaB${*V0WksP<@8=7G-Amy|T3`kry@Usz{+mhl_JkU)emuuzI z6h1983>>uQn|y7MrTNinI?5k=0GyW3?lHdmi|Xa5Iu)j*?N-5Fpm=@Y;V$vwa+wtL z0Q@GH0xY6{z{s_|e*Y{$>*byWZ&QCikn1!g&*6V)#zFa0h1`rrSp_V>Q`L!xaq(nw*P61MHn&$_CY zJm5x7dg{~p`B1+(p+mN_ezzx<{qtY~y*2(r6fi%e+=%B*Hh|sYcl@KllXSWBm1v8c zF=*FmdZYL7_(kq6AMBk@JWrc9z^FtMoaY(U)!;#UZ8W;muRBw6R8q`m`p(y4lC(Fc z<70?R{+(zu*@i*fDFDevMF2?FPl3IGZ+6-?$UR{oAej@M!hbb2D97O1Sg*dfcPG3E zkepq1{dRnPP~nWH4*bn!e~6l`)|C~RXb1i^SmR|k4_~cL2jk~tlbA9!3AO9Q=CXv*>3)K>w>y>lXfIA$xUVUx5x5vfRxabR#a!Js@3Bm{k5%&$@YMc=z4=e%z0=-}^N`$3FMR{P27#64vC61D-TK*>%u{ z0*0xX9^|GW3E3B!TtJFlwP!mBE|M2qm+QMp8)?YA`WF1zp`j|_pY&uC7qU^%cK6Z$ zN=JttWqa;|y%Aa!tLz_6&5!z-!4I=(aBv6y(FQG>E?p?w0CyocGEgHu3jh6o?HN!3 zmy;-T`5^d+r$xtVznoy%T7{{>)$uRoJ7g|{h_u(`bPwLIL9g5m|4Fo#vRr;5WxC^^ zvfOZU!jXai^jWrH(Y@lkL2CR5PR{rjxb>cB0J#AvB9Q~{>x}d(6MA!6F5UD4i4g0D zS$#X7&qV$sdBpOkXcP7eue^?F>tfBLS_;FTx^Rx@z3w~VK>(@w>mXozq>-!2>&qb zSF>%G$VP40XF(6Nk3@LUth$kW6+g;GTvypZ4%YYJM9FUYg5V?eXkhu@?Ohc9ddg7# zy~5Ud*_;Q(zrYbPY|CU&C;f2XbL9yY`_02vtSaHXVKlx<8wuPm&y^K>yl2_xeEN^D z|Ke-E+`jY+K9xw>;7nSTsJT)6==7W7E6@`r7s=)d;5f#U0S9WQph;%dyCHt{;|XB& zdcP2-ETS9@vQbT#((=8?d@xhXxlus#5@3(o{jaCZ|t-Qya%jVOh|POzIz( z2M1dJX02Uri(w!`D}8rP^2`_Und$&G9gQTzx7r8~HQ3+3?kaIV+v;uJwm}B=t{nIC zrq}EG|ACf$X(2W%Y%ZmZdZ?gkG7{|5M`=v?#jYjvAXoZV+{H^ffvYqy7m!saQ&ZNqn;p!U2P6i&ke$F7}LJTGSegXdK|0@1VpP@#W>XlxkmK^?z29uX! zA!Tm-pW?^pb6nfX4fP~;R5x)jF_Zmf%Z{=jOU4VJ2gPU9X3>KIBwsFj06Ys+4iYv@ z4rMsoJd$pHcBf2IPaYoaa=F(&3rePe8p`rD8H--G-*eq(Wv9V_j$eRl$@mwxb(FJH z*6+%`$-B4nx~12C@LK#4^l*D-cF^a(z8&-$odt}hX4< z-v(?FUaw%~h&E1Zzv8*@A3}~M;vb|Ivv%hY(9-BJ|UGPmd?**_|B-Aqg-945v6YW4s+ISAMO1r-n;l@0r{SF?L z^wU^G;S+q!qt!gx$RpKi{Wrd0_n^_kCj9Hjxt&mYu$OIZ(2;hu=S0ihW2HWC%E+Gs z6b_Eg54qd#9z5tncF`;bKXOc`$2Lr3(DrI_whTu{x*3+O$}GPwwdWZ^qhvbfo`wr-mLhyT+Aw`57XKc5|FB>i+!K+4}$Y;+X+cZYw{s(7}D z$+UJT$Xu}%@a1x#=D6}9eL|yCw6LyiwV4zHrl#-5jG!r*nFTe&QoF+8{t0W*Dnk{yQ$(#f{EbIc0$(4g4D0%DTo? z760>Mcf)bY8Qd6^d{Hty^>)}BhO?aLxnkpNzr$;U_ksUZDciNf$+F$!wm1oS5&q3k z^_2(r_TlW~MFS+K05tS_Kyt#<(HZe)`G02rr=Z#nuDt$z23v~Fot>F>{MR1P5hwZk z*=3dIB>veho#(#HmObRzBW=!=`_bj(`0mayd7qoSg-yTAf&tzCYl657duL8UZ5fhw=)zTW;Uu^GE^Yp}(AR?oppu!!v;9E2ZqCYvQL%i|U) z-c8x)xqq_2TRY2Av-&%U+jTG?MT}@H zkx|bRvYn`jn1S^FMS}qp*Bv%=s*^ZrS@rQYmhF+=RQ@dUvgd_wy<}S5l~DkjQ=b6q zv25H+{M8yzTel&b?Vqe8CE85Mzdq-8@|ztI#R%i|5+`dmG85uADS+x`$+|ofQ`=(} zQ(MsLKzcbYDbO0-uIoDZKPE0s=(}@G$XIY}DBt@5MF3VJSM8P4Pz5f#r0u>eA=hV& z$4kofU~Re=Mz^;5H6^OkZx4FimA&SXD(?UQAOJ~3K~$63XYK2Ku_epCu+7(9&&r?h zDI71sAF^-`xSY?&QPG$1Kdy1qS>#RSe{ve?5>-umxiSJe%KV}4e`UY*zx`kBo4)nC z1hzSOSwaU^S8=dSwz+O^rO$>61KyV*}CA}F7-(g!Z{SvXpkGPGTFDx9Ep z;-5uH{u}?PViZvvJE_jT6g|bE2b$dx6HRUc#z{uZN={~d>2g-6B{fzjXZI7}pBkP%Y?%clH!*Om%^h^^?tHD;l4``80 zA%KAn=VolpR_tWc+pKC&mK=mh{BrhGxU_%O25pfXN}HCyJKeTSo=HC+XE&{^F{2&X zZ5d|!AH;vm+!p>sKP1f|Of?{Z7zsR_XZq!$`Xz5FzixE8Kz#1N_9YWs$a}PTG5$;Y zOnF?Ev;8@fzKmwBv2vmGqI=u2m;0CS@M2gt*)BdkR>p*wh_%j+ax68ilCx5nm z-7onfd(XSyaf%SxQ~q7amkww4nC^LuS9mQd%K|VBuAi1VbAf3(rJa5 z`#{4vc|T{uT?i~7GVurbX+}luI?L!?dm>6_B9HPt{u3RdzKVvbUkt2FNaXuMLg;hV z$4*T#iqVH)9MmvX&i?yo!>lN46-3?DCs;P9%JeBQ>@zAl>!LHjc5^R0rGR*?^Eb+% z?W0X3P_|6pC69{K!+CsX}9C&Bj<-=6WG+f|iN`RPV3+m36L z-~D26K4l5IAw03KwpjMQ>CgS&_S^pOpR*5s_!SwQ&i@WC>hk~t930=EwvH&Wi7;z@ zr6XL8(cMa#2J_L6I8t_8VwJ6MF~BnEFx#@;P6ZO`?*^JoUcjim7U0aY!m8U<+PFq< zjyRy$_cr@Lo+nKj+=7A#!_l^w@tJZ1ymOms|5vt)Cn8TRkSrivf8$C}+F*?>;qNG~ zw-#yWd-$7vDS6t03^YN`m!EA2%3hQ`Vg0itE5ls=r{i|Y0qYdXC@uUzF$0oSg8+O~ z+FH9kKCgb!4*F9Z=~fBoBNDxIJMPW{iN_?AgA2kB>$(@zPQ@mKp_ zyY#ays-%kSUtgk4_m|f<)(`rWLY8>{f5g3e@V?tsAN1Mp$%T+6B_RZoP)IZ&Ni>u| z5K86J0#l*YOu3|DTRXNLom$j->(u&(jN`52W$LIcHWpDwrbU?A6beOct*tE)h)IBO zhj3511pTzwi4z&%UhtW$*p$%^NwABzUZ3vPW%R z_GbxpdINtN!HK`m{5JC9iY)j;=w!>s%|{vjF8>?<#@su9Jb9wTcYXe_?7yEzAk~d; z)x5-PKYU*ZAB-~od*XYcL6=zV!pB>l{J-cr$y4m~&-6$%Wd7OTocOyvWuYNi<&^={ zstoGcfRW*4t=7#p9)e<~jR&kz0%;Um-nZU?BbGcI689(Eo+uYxbY+v;C*C zJkaNMM)Y5!&hY;fD>#T+w~vEVkNQmjTR~?mx@QelM6&KEq_wf@l%!q23xlQ(9oo`)}`tRSL-tpB1@9~bEFQh)>CGDY+;Rp2nsLy^QuABH1f7UOO z3z9RS;_wo;*PQ^WR$zrPNV_ZDrVqeg9AXBL4J$|M&3YKl5JYe|B=- zKJ_+0Emeop%5GFDMpg{pLGC%Z3@}sT=Z-YBD>O=NQNN67i-*?uQRGi}NRD&%1$PpyMzy(d%CV^W2ES&H+!L$#``HnDVaTKlgJ> zYS-3s*BMKv=bZk1G!Fs{&~oQtONk?>S?$aSHbk~#K!M$MOEUjPdM80Cp#u;{$nAzW z_%`0D{g1vX|FM(Ty>xs7;C1nA5R}juLprdkT$!0UmV05K~1P z=-Kz^>ph<~aJlTk6p~}V8Ul<#%ku$D``(>qirrRkn)~eZ2>Ph|Gr%T&8vVZ0=e-}qDxKK-VAl9`w`-qD%!w^u126lC=i@*4!@nJ` z_^9WTPgVQFx=!Lxwlnz%<60M|QC*eKL;Szxzr=AI|uk}Y!l;~fBRyOW-qvBCNRAY-BA6G;P{akmpCs*jq; z=ejlFS0Pb@8gO>Wy*+pSP-ajZiM8ngZ8bJ_oo0-SOdRlHcy0{}JY>+|QIdHh0Y84! z`cLIjqgVSl<4?}`4*pygsC?TGi_?hrbtXv51SSmAefdXuzFYA=^gaTtDqB=1^K-xb z)%QPv|Lp7DhHv`TA9CC6(x3V2P5{a752{1`PDrQ9BWr_b!thgHy1bF?L)e7}CzN8d z7`C|A#4()iNcdOl9qm+^vEPYH)&w^|wnAx_^c+FI;Qvs16`|;LwaP6*{`aHc0mH6? z1IRK{U{CC5;Rgi>N*62W{7&{?oF)ea_w1A!yXG1D2-ZNgRxJM&$G-nMz@qreeWHT~ zUxm1xQOKS9a|wJl8o~c6lK^Nba_IYNht5ie&a7q?jO5F}x25uCUnBYV-Pq}~15Q2) z`WptLf8Y~!G2k=dHlxe@G@Y}vhn8jZBn+_!%Gn?asBtft%;kO2@CSW|lEQvo0G`j^8 zKBKGcA22Zd7{Uu;=AZR>jEn1#4N8Qb7{50Ucy&Zk0NV^u)!%p%dqcp>O&a_$c;q}mT5B-p5w)E$#`0oeCzd^3RyTs2zlkhJZh#>Sf$6`5F zv%34fB>uS!mpjCI*+ggWg8aUi|4a`x{|-;_=On*k>#4x$eo(t?&~qJ*6SB{e7$WeF z2?kl?M)BR0a$2A1#}w3dP{9D`T~(N%xm*eTT*vM!{^^~^J*n)*!S$W)t&Ke4r8E0c zC!}h%+6@v+`d8?+?ZH|Bkd;E1%G5R_GT9aw_mleE2G`?uwY~hl8!5HtVnpm&G9Wx? zU8**~)b?|AnVu z@h9bxdVD4MH??%V$`+-sU+I#~V09*)EC9gYddE-UKmK3-U%dBMe%*jywaqGV@2+3` ziVg0T{Ucvgk2NulYeECFXpc2(5czn-_)Z&yS21TZ>a_Ih+Cb%q4j3hSWAC#!Ppx%Q zpXG|%ZR;d*oncR~A?u7)$u1=-DjK(~MYVrf4w6g+aPf?aXi&iYAi=LZZkoDPDbY&@ zp5%?#X$9ZI2#dwma&g`m%gd@fY@fi4}Uq(>1KrH z2I0bJ(BB1OB!fJgKIfE~b=$SwWG7Ath@{fVY3-Bh`o+uGId(|^>QeJ~?x(x)O#Pfa z{;vPZ+6bN(jrjP#%RYx4GZOL(KuK?k!RGYA7DKdt>u!AqOiN7I{v{tq@~>iT1lr~I zUVgr+09TO-=l`TZBKr1SMVSUhe09dA`-Wu&Nq8^;C;3f4ss4i*C<-&NK0E zyzx`-gJ;Oivx zBvF#!3uREAm4rPhq8O87zcW4Jvf58O2!Uo6m*Mt4bZ{WIZ_jV`#RljQ9Nv3=`kU)! zW8Ag>WCjX8SXXCXZuKv;Z0I;e{?_ll+(Y?auR1v8FCJ0;xt_|B+MQ%niEhQFGRlfR z#sJ>^o?pdR{7>J8w|@UmlhI_Se?(f0BV+H-xnfv@Wa&7H76Y7|q_nY1ssAlrvRghl zN;}bUX>UL5oU4vj2Taj8x101|KxMv;s3x=uT!QQ;o09QQiDagCF!?BnRWC+i-*5tU z(=mke3sxRi2mskDXAfn#D}NCEI&2 zg!w<qkgMzBGBcK7In{JxuN9l3mFY-0Dt%1E6 zpbf+X5FV_%{g2Hy1Ak_(%2*64?e1o*An)trgfy!^%Z_y5pu!$-d0 zIUB;W{i%N;j#up%q(3trrOo2FQSa|PYXg8oo$hiTDa_f&b28fb41R}0x$=&GY<+im z?fmb(@?1#DRqUkP?nm`G>>|f>Opi>jyb@p(+E$q=>|na=4$U|EMek)rQhHaI&@qt8 zitlvtYy9QDaVFgm->UuTB%9i$OozA+AZxr8??sOUiT(a+hsg?cL4vUh4jma*uBejL zpmLV>xK4DN6%^jLUDj@w5OEm|%Ay~6ziy=cT>s{%=G1n%Zg&Y|Ss)Jm8b$in?q5%( zF2nP5U1$8ozTr_p=mrj2{&(`VJM9M^SpLtJBPRqWb03bKok?e-9^zUUe^s}y`lj#2 zSAFw$;R%~Xndtdig35T*CbES&)mAV@*?|=K>MSf!_#=?BA!z9`q$qGhzt~A8In1N7 z3IfZp-Lw5e^EcVQ4E+uJB=o6hHvCa3^7c0()&6mHw%6330xOjz$P>Nk{xWZ;${FL5 zx-N1=!OE#WD{z{97v?7UzNLX)Jv)2v@5KIW|EgM5UCoh6y);o81z)|d$|FnhFT9w2 z|J?zV%{H@ac%8=n@v&noF`HO>_N})06LOo(wFJ-G-|gDo^S03$hUds3gK?f)FQd+D z2vdW8vCDn59)&IEW7UYI;WMM}+s(_j^b{j4e4;lX23X+t%V%)X!QXo?xN5>{aALw| zi(^nn6Bw5p`ZU7^StR~z?5@uuQ&<^fpCb3Ri@u4}oL82zLl=$qO}*g(fHB}3R(jGr z+5VXh22OL%IOPGCtNv@F=j!~?Wc5rz&;O= zeGo+a181G}UtM1Y@Wr3~YWz#T=QVi7<*|x~Q@R;^ycPduyVS&g5^rrBvyZ3|QSxsc zt$=J))z&jj_cNhu!-FpDFrQTV7I=?lskn7O`zZdec~&|o@c}!S4hfR0wto6SWpqFw zYi>VS6mR2dCsC+WZik;aEfNS!9}e|;CLZ49a?mm@&D3qy7-9EycYvQomXKms!*y~- zL7pr6nd?fJC69Qx?#+DW_c`dy{qm@ilt~J8bJjK)5a+=RZm|>NJ_f1*YvHFpqD&r? zm<3;?_UD$sob={$R+gP7*X_#Z>y&J-R&LArley;XVLRfLV7TiX4sz7d((6Ydm!~dS z5BjcqswnrjXjkPb1$J@No6?Nx2p?E%1Q|(OXye2PqLK>iRhPS2&9P`U>$5;9Y_pBI6mq5 zpZaUxfE%~Qs3Hfk+L!-M$QiyoBN#zLKy=az#!ZqMAjv78@f}C(f&7crl@;*5RD8X) z*-UGj&#JfCZaLC#*v{_pn=hLkwxjOs*gw-JGa5r?Q4Lg*hdE=J`<696p1%kWQ2*9Qi0*`EsRC$W@#HGA1~U#q=sdH?kH_QUmPRf^^H z1Vemdz8><40x)`x;WOkp{t26K5VUo+BY>-K*uKFAHH`Lr>|yu=z*a~QVFdp&`$h_2{88Jw>)9TXa-^RPDvjU)DfsY} z4AUM|{Gm9z)Upk;xPVv}gB6>7TOaJC1VjHtANCyldw<|F@JX+Hk>g+dK2l7Ve6jk! zh!0$k`L_X8;%W8s503wH8L`ns52IZslv8zTxpc7~>dJ^|qW1z))2lx>9+n0*{yznP zTvurAWoG@2+-8O{(S`igkoP(e9=K`zTl`;0v)}W;WPrzVmFPolNjo{%Y&n*5tPoVe}(lO0m-79d$?k?MQr1N|oeCGpOGli*llS`)NDtWUuB{U%0g zqXdmWdGEYx`gW2LQ@ez;OY}L2Y)5Sm?Z{td!i_-l*86+)I{Z8OJmh++Fg%s(p?zoG z)aQ@J|Lg=>KHiZ36rok8(mENwLd9EW<9KShVyk9REpL7R_~l>w0RGHh{{QeT-}b|5 zyu@(9H>^_mN0BM7i<7L4K;J8%7$@t%ok~L+)&Fn;F*=%fMo!4uVK#68YCx60Tp$hw z*vSKpWHa8ADl=S(KCS)-MVLIqKyJHU#nwm@b!5HHsGq!C?dNR&i09d{Iej31W5A;C zR{{y*SHUC6bfH)ARoY!|w>x!Z+U8P0t&$*@kgr&@T7%*I?m9Iq;ZZF#!N6J&Y)QMwk}=0OLd3?^@F#h3eMtIcLFX89 zVt|#ex-~;CMv~uxA8ky?oA43uk$8?d$yZJfl3k85V(ka5hA>`4ax=i6KrERh{igwo z@E7qJ$JpeRl_q4AV$gg5G-#5o>2)7{7)NwY_x)Vbj!48@gH2ZUx4-sd@Tb1)^?2TM zo`rmM_OFX=s}IsLPP_Cz+sDCeNJ2%FM+uwu{juVHMZ`*LI| z^j*O}%2|6%-x>(Q<)HYXM2nmFr}&TK-!}IU`^WfK+hp{~HZdpixm@5+iTpu9ysa)M zV^L_@Xmbv>|0(`4P`Evfm(K5_YxU=&lv8_|>81F0(}|!{0EDT0DK&17zA3}IdpAsg zC^z?e96*qAb>P8?#0)_-3>=WPi6qfM5>JCD2Y0Ejd&z3X|1^kk>;38e-t`-i$V-3d z3}(gi^t?+%G#HBa;p}enKdfiRKZDw?N0I+rr@Ln(7J-t|ivp+&L6+6zH1c!Di9q;k zZ~0ODS6}~D{PM51L4Yrad?M*)H@KPe$CPPV6Lx5~_T}+{SJUc%mAli)8Y$ z<%jtdHe7$JZ=OU%qM`{iZ%CJjoL9!!z6a~?vRknk;Uxww#HNCl-=~E78o-W0XSqvk zW3%Hcnba)<7=If`&`$yuA}Q+*jQgeR8=#!{%N_|sXZ~Zb!gm_j^=RJ$^7{ir&anqJ z{+EEk`R{TViMR39XfbENCb0tj72XiOcAs^!EUE9}KR>#1B>s_Ipd_AYoR9ardNR+# z1LTzk*4CWq^O`a6+~+(CU;cYP1;6X{A0u)MwXE_^eJ|O+?9UHnoVH9;vN2tZt+q4# zzZ$)R{XbpeYolzj+f+Ue28jJj^-g764vAh!&rMJzJUH3^X*3BvFSL-3)9(bdC+NBJ zfRpSWnJF7IcsLz}xIzRxT)g(~39dR@h_}TZqA7Al5z6Ia$ zeeVV@S8}6YaMgy}B!}>I{8Jk`SVPG$d#~)26`JB8t$pj)s79R^87niTO6QoTI5w%X^3<5rwctR5t}ck+!2$OTL@uvRdTIQ$J^-KuLFp>2Rsuj14N~-Mv+Q}k zAA^pq12Fe)a(h-cX0LbBH@q|Xo?L7L$&V*c+x~^j@7>1tZ%+0NU(0*OhOWwiwX$hLmQ;7;IwMI3!NV^hE669G%s zeb)o$Td-omXR9y_|M(PSdJ?ShK(x&R3Rd1WZk(`9DpxSIzYOPQ9ADIK{v-q8` zpXri*=bWMs03Xmfm=-hu-o(FUkdNaZZkvpCxZSV^>i+^ro!vKiQ=0lS^0PVdzjc*? z0!9C(`SU(AL$bgNL$gjc@eho?t8`xB*sT`G6AfHJlYPAp{B!w*qjnZwC{Vk02bJ|&M=_BsQuNpa=R6W z+jTye*V%}sa>>`#cvhW3aUQ80-vdv9|GQi#Xa4RW|0=_%^{DJUI%S?^r_ujq`OnTl zLC^I5y1)A)_^-eIt$6?APXv7IFd3^cwCxRm2jXV3&CpC71aMHB!X+|%qox0P_4YrzlQcNi_2jZDY?3gC{E1ezyPAUmt$2ZcD6- z#6dOy*0~7>ZP+#^XfOcEn=xo+GB}#QBhITKIHTS^`OL`6MhbMtyv1_Oz!*~d+atMIA`7F zwr#^k06~isS6wbKXi$$pBuPn2-9xa}KXH8B6xrsP?D&?Q_d&?|f2H4(bc6%n8FGQ? zOY#RGyYF8J|FLP4t<)HZ@A4=2GiATR*zmAfakAgD0<5vLw8ua)E1rD#T${C9t!cp@ zkc}aIjgFZtr@!HEG65gp7+ZIJ(?v`?`gHa$f-dta%u2n z{U`2s{#uE(%@W1`Mg9$_^c>o@T%93yKTGuAZJ=u-*^u~~{6V5y=JM$`hz|aoY&QF} z9Cq~nzrj@h@U?N<#t7yOgztiPK4v7oBZ*2@@}2O~=I zpZe4mQa`LnOZ7A4^R~5dk8s_iUt3NTsez1XU1+q*6{myS{5NN6 zZAbGwV>B8hxc`IvBNeEM5lUmn!wVYnI4J6{C98FFMkc%Ww++`8k-jo1b$(yU~xq zOVUr08+N#pRi*)`p0~Ceo2f7PYc;0nAj`s3btIaZOd7skXXCt(~#GhMEJjNd@aB27w0!)8o>XSiN2(dP9Ew?kuC(lz(s!-msVDgYvn zHtQanxLsF+E>j$-1&vLKuGi^0d%YU|doaK@w%f4IE9>J`SPQR?D}2TubNiF)6>EdVJy=BXnBg_X1!K&J1B@pdW=pKE`&c|o zwX>7tAY$4pCa%sO8?0HY3a;U6#RgtiTSi~Nd+n}AtvLr|blY3aw)7hd;IJ6q6g8@uK)5WN1|L?LK z*UZVs0A^|=Q8z^0SItxoNx*;>~WXs-bqigG-&mQ%?UHAE4c@Tq050n2W+VvC8v1=<7l~p0leuOz7y|%{E5KcBfJ9-BJ&O!@1WC7*KQ+> zQ^scyOk~J1eF_XpkI?ble40CD0thkfS(sd2Wst8aB?JG!+y2l9Ey0-e}Tj1g=S{EQ$+__3E+0sgEvhixU$NB((Mj^Kw%Rd z+zl~Z7||qO=h=@u;ve)5=t4iZ8;5ZcJ(Byi_cjpf$;gOB&OU=+OM~gVYn}{`(*Q%4 z=d9lo^X#%5`L_eeHH*I>UVY5)?V#`WDM&Ho){%c#(VN&C0DIH)9L^a1DEQ$Z+XJKc zF5O-l-ad28biGF11#Oxc+jRC|InU5;QSwji5B%3}-Urqa?}#g|G`oIU{h+c@XOfK0 zoouO%QojXRhQIka^Gul0`|1M)y5fZOKTCZq^@qKgG3c=97x)kUyu#4h{1~$Qon+kL zO`l~1bC#}@*|rn|g!;#4C3dQT%Vh|uGbK)HkVXQ3aDtp?z_C|kSk*|VB{{^B>t{SR z@W$Wq8vKzjd`;_TWWa;@pKWyFR?t~zpSf-~L(TSka2wouiU0e!+R79I*u4|ERGDTh z+Zcp&ruct^Tq4yC-xECH@};Z;!Tznn#PO@fKdEE~gf^5tQP?h%i-Zi9x8J%#Lfb$p z1Nn{8UcRypW_@rU*P>5)TSa->T*b5Coys#)!Zpr>cRUAJM+c0Gn!HnciOQx9h5t>Q zA#e_TCvnGBhH-~8w_i$<*g-_<5_}a(ZY#vRCT;ay-#+}_`OzewOsG8jelICklBl5x zo2|6UeVwj{!pP+_%};H1`C5nH2lKj>BlhJPR|sBHWUyDDb*0rIEibsf4m$O6#(m$C zfAo%|lrJXT;q7e7(N1^GkL0kXf+TG=P8SWGzxOA9319Km--;jk>0gX?*ru8#Ds(gY zrhtLb&%jB6T3#GI=-Jd6l~lrsmmNiyXwThdW=J;Tat$}hHAu*|bfk(ooJiur1E4(0 zt^)`;F-=~`YB?y?envmc4suGx;U_f6Rf|LE@JX`EZXh?WoD3WtXL5qxs9hqk9LhBG z%epQY5&ub2$Q3KFr{MA!ZQOok?}G*X0D{xVyEk5}1;Y%=`u;kOy^|x_XxD$teU7?6 z1J*);Nz~l>e@Q#9BbMMBoOX_SO^^}%pBd>T%f4U0paeRfZ$HAOi~VE%0I;R<{Ci?= zMmTI&{>}d@=mZ4Nx8lmOF?IsE;jlWidv%7bG%@+__JatUur?-~3IDL6(Ra+OZ~hpsY%_n_cR&Q+?n$;o z9#&tM5&B~DbTR0aY!OTs116BZz3d~Nk3anfJ{_yrE8(_o=OD3(8$mQ z@7!0=;9RCt*Uk+64E(jNKEcfa#F<{@vbe?khRX}Fh2WVTkyCpuGtks2%(BOIvO;nf z^2GzG?b39OIC6(&(6OZ#O)0kVPqH@*G$x$_kgq0@;Y6$|+D5NaT5mE1;#9|koD<;O zrzRSNG{XntBP-JE|2kvtJm;%R`nxOB>R$U#HZkw?{pq|OrN1(m@<9BRk1Px8*^&QZ z$IkTEBq&<~+?W5>f$Z+kT@00Kg0xA40AB=h=kLsrU(WV`U_C0h(BM$aI z4+L_ckbmF2$oU0gVGy#5wLuQ!LmtRT)q^G?O9!}^Rs^5Yor#G?HjF2C3~mXCcqXL! zWVrVtL>p zj?OEwhA2SLkm+HsCw<#)Nq-C1oP~aNoTAl;=};w3DJyW#OQMI$2wWE z5(8OAe;M*@^5MkREqf?;; z)4bAm$~!{X`M%rFrvP*#ta&E1b3WxcI~k{BRk&_OrVUsO!sNJfGTFlfa^SrIXyHiD z6^%pUO6{GlbKqNj=&eD4C;P60=VpBQ2yH#2e8s>1e#n5#!}*^rXmcf*%g?LH@7f7< z?MFa~1`bg(B8}co14&kzcew`vU(invOHerRJwNt~_>aEk+wlWG@$(*32u(7NvpzW; zN^efBUhvo!g@f>R1pnJ|DT4utC2pHQ#?yi)H_R1RF=O6lP29ZjZ33j18TKww4+lE9 z{S*F&_D_9jOu;a)ZEqV1!aUGkbaqz<47sTqK#T({jIbxk+U9}*_ z>Ye!NuygQm3?kTHXSh29FddFD069y|>p;$J2S794tGwf3XWH)*1U&e;>Wuo5|CNAX z!Z(uZ>^kEa?Qi-f#{10uuJ5ZCuwcBK6@Uk|L$8fJV-;e3ge6*f>~GvT~(qRE-ZEbKrqgvTa%OOJGRJNsO>;~R!c zTI#R9W)D|vy3Ea29{h<4Q&Q~3s+jF|wjBMx$=xi;G!M8~Z@!`e(Js@$coQ@J>#r@C zumK1kR8aD6^sgand8vQwI_Uki_QheIA)cnEV*lgVOCB&|{H^3a`mz_|PyPN+!^gk$ zHzQv;Q7FmT27q~?R0~g35_iKZ+iL6YWM5r=$MvUU1rWo9L*C zZYoR9K+j!A4t?+g_8&7BROix^CHx(b)RV+*MkjSThPU&X24DZT6 z+H!ej-)ga8)D$t{&%H4%u$6d*no4Zx;8iCbuMUH0Ou=k0!{6d-{^k$jFMPw>@&3m@ z;0CAg(r7NaYGi8UR{G(#SjYDh##fAfO1g`DWk)zYm_D!Dp$=Od!+V6TCtF25w@&n^ z2QlKS31=h!6~n;4HD+2@>p}X;O>lt0I5U!EG@Sm%621WHqzShY?oR%_o!Rg|Ir!;i zGJ!gx-M(T~%&yJ$Kfwx-f3_3RZ(Z>)xHAe8!s#=#YuWx#q8k2lrUaiqJl{ZamSpz? zq6a4|%T&(72(itE|IoxWWAAWa)sIgU?7MtRgcJV>N5BRKsxNW9x0(Bra5d7%jnHv% zia$K}wG9#1`}KS6Tn_*jpWTmV(exPby$d|azvP|$4ZdPsf_4|8y_K2(C$=$QqBuw3 zzW$Y+^ovYY8|KGEQpU}Ls5ZOJ$&_JZd~(sdvM<~6?mOF~9n@HIvaP3y?v@k(tjyQsVmt3spFVP zaG#CH9U2tPBmANMXjjT+_GtgOeTjcV=FSd~@aRjjLT)Zdv|9|9x3>f|F`m{{~Uu;M*-TdPxajAqUuzq z01+1^;FkgbLf6d&TLEyI`H`4u< zSPm#qNj9DBb7$+BmO|hCPg28*vKV(V1Z2cQAK<^=_3mHBfA}@uj_>^8pCN-t_$Pqa zfn~!j3G4`(^zsyy+2)$DtYq&DH1i^P0{^2)s*HpSOkJ=rnr4U4Nd9gncO%aM>*fQP zR%iPceKjNRIr1p|Rki5sZtlz435TA@bS41AG2PIl-v0OY&GFv>bfBy*woRfbaB+fc z7*7K(w=U*!CP3D$+xj2DPo7Q5G&8&`*^JrUJ*$tE2wO5{N2}%Mx)R1_;FSOqli&ab zY?nZTW8Hg#WVl`T{sNeBv>&ZTh@<`%-kki}EVS7v8_S3Q%=oLv!0}8sh9y(N5`dF4 zz!TXCz(407`~RIQUU5zM>&A*;NCf|}zw_r={*?IKjDMOV-{!)@zJU*!U;8jzH_!t+ zd1jmtM;W^O<{5txw9RMMm&=IN^W=!8%Y@M(J|CD9F)+pc-t?0acfsE_&Lg8Wrwszc z_wojG$j~JDQ2viGf)AK605uW1gCOt;Bx{tJZB z&-eLRj_=a54ooa4{GGro*d8vvp*wJVQ?CK=C%;9ug0(CNTG zTFmhW(};aA=(pHNB4ii3{@m}qt8-sWbxEQV-&oMROiDbu z#IwUKU$^jY%Mol3xw_=Jga7Hg?hS~X=yoc1Z{VXilLJ52<=&j?fHb6ok9uMGKUfgu znHpI$ATpb`ojaHB>{K%*-q2UU6tzr3USu9l8f^#E7;Y7um~-Ik{>~5KEC1R%@V;Mr zJdub;*bSaRZK?nEK+h8(R>)L8VH3WRM|{;Fwi%)jikwU8za)r-wDyK zIzP#m`zPXOi3upE;%E2#U+7I||ESP_@66s`tA5y|l?D1P|5)%{r|tf~+O?#L>Fvj} zQ8*$S)(!gRdMCX6t+%!1o0j=;WdQb|!x(hFImuN%Z6FaT@rH$ME}Z8+&TaqpD#rhP zzXn9s2k<;-GGN+i_IwYZT(1IK%;pbT<_=1D4HsJ0zj4sPzKy@l`i>!|9dye=)th8W zL>W7s!P}ja3Wja*px=UITgG5XzikM)2a?t!#mxsrB+)QBgzN`CszJWdpzS3vV)mP5 zVT{lbigmXf0TEDdsjxb*I>DcCf7A6#94z%|pTh8GHNNu2?Vt^cklmY;HVb5)Rs1gl zK#{eH4}JDC@rSQwzVe?P-!^8}It3V|c^ zg>mV!iO!~s{usa#SaF4fC?`~^SQ`;j?4-Q<{8s;TPevinq6nFaaTo#1jil4?W3uu4 zoRs#)EyW*A{nORs_~euOr+Z-FQ3f(jG*ZK;ij(Sw{?c3Vt>66<&W9m&0wCrS zd$r(MHFmf3RAd6$R>nFoA|or;P5P9PlEz3}FTVVp6LhQN zzddnS?O9UHe0JQ}PS5Gf_WfeQmw*QjJP*Fk2|a>tg0s%57aGNf{O&|=4BKs;zTuzL z>~7OfTVVXDYY`ou*}q=>g)ez0JqeQ4AoYr%-ENR})#qdLn9eR(iN^aSZH8yRIpMz$ z|7FF^?!#rVlHQ++|EHPr3valH8Cwewy^Y^X{wMVm?b{}hIr%i68u~`VfH&|Tn@vkc z{6{cEm^=L(O?9Vhz{vK?2nesc_W|Fd!9E?Bu?j%;8B&jhE02{z(_Gz4C60{U&2wkMy!o;P+%2F)hGx!e9&^qM3f?n09 zg}c@SUnhFdGE3S!JJ$QmL%L@-liH|sSe=WWczvZtB>Y*jZu$RC0zvIU+Nnf0-HH3d zCjiw!oDzBd&B;JFV^kA7Qc7yS{LYGz>aC(`K=L(P_(BmzDR&+y;6A(GU0LTxQzylS z6?UAA%Kbhc^|}Ykx!z$QMraF|TydS_;J^d$U^Qd| zsAV89p~49Wz1S8HqOhHwoXiVcJ9Ud zB>b~T4xwg)u8!!(iiS+4pnZzbQg{78C6fo_)FddSAfx~)POA?qx|IV46SsA?{|4i3 zaR&MGEQmSmE=(@C6rRwt!F?qI&=KQ+KhG+gW4AV|{=bqDC~eB!6I}_nDv3 zugSkJlXm*5{hxrJ01!)^q4rq}4)o=eo|24pJZVDJ+1uk)ioG+{&Sy&KlX-C#_xLl$3z{Y zZ)bzJZPW8ZeA}%8u=j3@(r=y&idX?!-e}>@G-NjHSmTUOzZ2bxe`fz5%t-z($}{|8 z|Hb-7U;ak?hT$KDnI>I+(+zS*V~OT2-v|82pGR9$Xwu`)K9!T({C@S(&GcJ~HwUZ{ z{Lj8tXqD)|0|PQhfK(ylF0|AR+SL!lNFNyHIKp&(k}YfeKM#9^^cb$IRvwf?`#aGM zfxqfaqSY+|7#-)j0#FvLM!_WTMH1j-z)p!S8v=W8_Z%cRUa1c2MMx3V0RrZ1pxQvS z!gCK^HJYr4T~hmi!GSyFZ#{pM>&dq95cv4rqu^fx!^F$#JQDv&7C~j>%9=mYBQoWF zuXVzfvs0*TX9ofgYEn6u88wDRk4)Z7mv|N(dVT+gK}x= zg9nAk%X2f;zTXmmlc8vngP$V*O;F*yM7o#X>BZa$!^OU}xA&~iA#bZZ;>bPv&OU!4 zFegI(20XA4tL8!IN@RH8DmIy$GQ%Ey??l%Enz`G%?Dp*2b3eWfy6gOgn`v~H#hB=P zkj>V!EKKinHDRAb@ahO`*jF1@=XWIHn)ItZ&vtv#ry7BO;JIeKnTPXg8zQQnWluE# zUluMhAx2H{hs{LWXCS;szy$#l1~AfwekuOr#6ouV7rrrXLLSCmI$=lj!SRUIB6f72 z$tlZ%p%wHY+pvL{KA;oH&2`K7*z(PTCA$FRqywMvcY@pZF5sRoL-BFFx$=qz|Ho@YIw3LY;Ex*w@dly3Y3m!q4K6Ex+*zs!nr>Ql-fH53TY^Zbx} zl>pw%@Uwj+3h1B+q|GkrUp?nUx6(TaD}0cM`fQljKRct}=PvA{K`zzntU_JlZ|?_{ zGvc27zqGgM6L`iu;s3^82a2+s4}G3!%CI_&z6U^!9K9h<%;Wx`m`JmQsoh#Ph;?ru z!6BFxj`jGQeBOIz&&?=tIlcm=hb6kqhbNPK--03kerWv<#+B)C3zkZ=bdTgnIO0a; z5zxt@;mX$6oeRlem9LYjM^mQO(PW@6PjKl`2IqY9t_WtAmr-Ssig#4ymVM+2JBUr8rZ z;_r$1h~Gk*u?GhTd`{8~fc1=Kx@|+fM*JlY!@}u8gAnsda#5Z~oq|8fsSQ31*rt3# z;O1Fk3w$`S<5LnH9>+hvIX9RlW-NOjBUS+j{~1n_t7-V{ey<@+ivLx_y(Inn<;pp) zo)liMw7eZ59}&$ObJHexc3tWB2>iYN41W0D4wg{E5@)Ym_*MNqbw40G=I>|1>6KhGux!`uxff6LvGO`V~me0s*me&lb) zpZJnb#wWel60Im6aQhpB@RI-J@-AJ;--dg0-xj^40fIhAP{PuCp(oV|{HYy@A%F^~6-$F9 zR&*fYKicxFXYS`Rp2z15VoLfQ=N#9OzGazUqLJPmR{%(GM@n}}&}4=-2YfS#whOsH zi7;(|+tEVlKk9?;Z~DrHD;d7rhS0UCa~fdC^-6Nm^u1YLbRb_(SEBz=I3AAw z!v<%#JYPlDRv~)}mB;>`W%@WEkP?^jUZE1>XTfH;Q&C^y_1T7l$`r5=zoSgN%%JPN zHlnnNGOlOYvN8blY|e>!1^&w4e;5AT|MoV#@7Er8XOS$jB3K9j<$t2Ow`Z0(@qwER zlyc(EO=KslGQ)!>fXUpXc9YLxc@~99Mi@;0%-1{k7Xi>_)ww8%=WKL{oT$sPR4OE{ zxOI*QNOY`v%lPj{A~QN$0ELHOVZN$NRcG7LKblX$O|lQ!|A=oJ5eUb-d-~6TSPsD< z`13nUe67?x}<%B4i$beKz1L2w=0kgYY-uUd`-J%3b~d^O0HK$^T&ul*9&zi!gH{_zbTO zUy{EW*ZNX2D1&Gi3AWLTj~md{05&tuNi=`fGO)^stKm-~+>R4GhCdIkB$#Y7M@(Jj zZ#K|n#PD_jCfSwG7t#QOeKs+<9ajLNMU8)Kz{-l*>XYX_`SiFTllYdp5sX#{IlPA@Y*D>&h#rXW%UhRN=G7qDgczK zNuLJU;;VU>qtcM!U(^9fIvKy@AfUm5-7|rU(J&Or>8LHJ$>Gf;KmRo-OFOxK-yWK} z-80qkC)vgW%A78dg-5tf?cawzY*D_?vcYZCa)9vPWh3zeAem9QJ0|o+K5v?up z+kbp7N8Rq_AVYZ&f=y4i)G8h@C>=i&U7!vr7wF|b+rNOTE^SM_yK|+!HoX|_B%+&6Hxi6-zYvdo7(V{yWtd|UyCgVlPSEhp=L zp=0wm`!ThDs($v&aOX0DKYc5`=n+fNftZ!&Z^JE#ym*JPGkef$(e;Y8>%Gz0?yoyh z$Haa%gj$_EnT|-}HCF1vOXe4yG|`94q|)CLt6eUgwjOZsO!(ze?xP)rS9BF1d~69f zz=^)|fsW#1T^<8vmLHRrgkPgA#(xa>S@mPs3iEtST^x_-eVGv15`);S5Ad-eJ9ZJ_ zwl?4)H_@Ka{j$H0h#CLzBQ=4V!bgVD+OVbkR?hJ^xw&5VCnBpK!>}ur2aX%A*H}HVaF|Z#*T3qe z_>=$K>+z!Je+VgrRF|vzqh7A#{;bEhVpoa7OvWkxdUEkU8YSxZQkOoI101lhdmZhg4+fj(+(zp2lw2H**FEEwgW=|$Vd(mM$n~h@JJ|kVTpj)R z`Mr8Tl8x@m!^h__ z?erV>ot*B*)~@x8etgf3!*83#&1c?UDe1K(+AHB-$y8p^U^6)yK+!Dp)brsn{IlwU zKdqtn1b^%V9X3c}c=`rv7fQ0=GGw#+{F{S1w`*5U^5bm!%qN>*x2j^-F}>i}@-Pv{ z8u~2wbq#IU68i=JNW0r_|7WxfgdKp@$JK$HG=Q=h=c^n59ds#!>sP{;X8bjojKPqe z0C}zp&i5;(4=fqZlJpzzSraGZGl04A9pmDC_y&TYZ~J#+PE)5Z37Z7bh*c7pfDI}+ z|0!8!e$C0OQrMj&wvr$QH%x~<@`b+%fBX%fgwK5K%d`&hU3?Cd+_kShd5>q+j&eBz z(326yKP4$)Gozj24^M{fPE3}4QEQCJ##lK{{MC1?@-iU&-ar(1j|1%8}J@Smd(3$DS4Ge9g z(l^SxtmkKrb5;LbF2aUvgX1|55M6o z0`h2Z5K$j8cyJy_P+@LI3aO5xX78P2Ix^k2wTYe`0ku^OV-CX8{i%%5gJ64bf;=HK zzIAmu&@E?h@9N;V)ozdfiT@N>Bwn2cY3%9;VGOD&WBZ|6?Ddd-qdc3tEp^ZV4cq%FsLZa*cDbW*+~*#GAh9o%q_n^?msD z$Dh!^A|q^6&3c-ggpt15y-wOSk&JjCk5ornj-fNc`d9ghc4IVJf*lEjqMXcPCs+k% z6qBqvMok2WA?q?*pAl>~PqBKDv(Z_tgb(zA6486T;V< z_HPF5eIq{BBMQk#p5(!U{^UnU^|3YVs0K6BU@7FN$H~!Xd zEZMhqG(n$2+&b$Y0?&h3s-q!)$HX$nn;Y)XyDOgaA%S#5s)%{<6gvb&tPx57$rhz{Gc2mC($?aHI8 zpZ$G~{~JG~VQDsV-cQz# zFeLgsomb_B;&H5Z1ApOldfv7D==@iCR0EqGO12zm>W~trGI}4+yXAX|Q-I#tCUL`! zJ#WwtI(orqitK&hl?l(f>~#qLTb(l&?uUjcy0X6+D2-ZB{zGWe;zclU?e6M)4*P}5*OLyK%=Av(*~^uJz|=u@ z&fZYMNlC#?%kxt(obh+Mt=c~82*~pG0mRX4*Wk+-t5e=Q4pS4P%RZ9N1~si)K6))S zK40`013T%T`@8PxmO z$;a~b9NbN#3p_RVXPN)=`Y8C5PUawzlG>}x=-YDjD^%TW^+>euM=IK?ip3y^6c1{1 z56bYsxB7pxJ-t>7{+l<~Yh6`Q{Xd~UcDbv5O5%3Wm-=6m&m212Z@>@4*Asy3qcIa+ z{@3;o0F`U~W4`v0lM?@3@u{!+X#DG6^g6ukMbFjZr>4^Lb~RM`ZM3`}|57rN@wRtt z|57f%iEQ>6L36T|H|$?hfd0Aozmxs1<@kM9M{G5PN1c7go%dq@GhN!o8;91$Zj2Z4 zujt?{|7Tq=|G8^T_%*rhaIMKNuMF^*qR3LinEVV##5@q0-7?i_<=GkVct&;!EtMV zQv10c!*TEZavq>Enc0nTyz!1Nt#ccuhg?sk?xS9n4rGfS5r5;M3--|23g8C+o4`Wl zX^_5DS@l7nByVzc8P@*GhS1@o=GbOCJf`A8GeO_mvC86*nlhOe{&tKOeeO$~Zs+<0 zZKYvN`pr31hY;v9?ZQmG?T3C2|Hc3MHvHgE{6e86lKf;mRc`|g32b(~6!BV%f&<%F zaYS4)1+&E&Q$qn?7+Ak=AW{5BZ4X?JTzKK{XiBYij!X2@-|V!qxi7esfRMo-%6}KY zM0U>DzPdXZ7zJ>FqN4ocVi$8->3cK0CZXI7iF>F*aVD$)@*< zK(rAZZ;oj86r&t_w_v6{6>KkrUD*^}Z_2U67gPBi!7-R!@zl${(fqf)n zY#hio%P^d>cc8t*Ys9xI%qKwT@8}E63%^&qOv_G5}JS^|$yxDWC6C{i*Q-w|UG5k|4DM!=3ZUSH|yT6K60HB5$BaMlUQqYlH9jtT0BNn1~n%BA3;|xQ|L+Y)kyu7zfgNe^a0a|;=Mk`Yuw+V zY~_Z*n3&cv(fkFspw?Rh0IZ;mva$p35@A4RVspK2fWYe3WW4rS+CuO&4+so;*1gXLi?3UWzLcZ> zyQ?2mcvw3<>}nhDT~BpT;9mU`{$l*m^S|roA^ca~@uM7IF6q7}HM-pYgOh|2DVL8rzdHjV*pS)@)A%^Ya{-8t19hz@++-~AE% zg>QH}-uo-RF8I0q65|yGIGH{f;MK-9qvl9N{#ZsbdMVJcwz7Rp%>F2Y)Dml(M?wm-}q&e-0q?GjgI%{#CiiVC6sKpENG{L*WO@e=67o zY#ptFB#rz_0H=UupB#ZJs4(GA&Uwx$=fkg!<0fL)wvZvFwIy5@6xl3w;8X*a3-J}D zOrYkwrX#*rAG_VFm)W@_*#YCbFN3Eu_t;M`1c571llW|_2Rz}Yo!ou*{TYesbe8?t0^-6 z#~ux46T<#ACo{-N59)g?;a|XP+S6VFdZgqg;D*KU!Wd4q3-%Iu`|f0$Hy$+Nl?;yC zmE}2j#-{*WCx|_AA;5Dyg}i0CV(q{#?Vhh?7Ge? zKJ0nV!IypRtMSIq{&>+-&~MfH{t}yYh<{JXe6HuQ@87;)09{ix#l15alOVr=oS1w~Qk)ozCgWxaq_S+HdR3F9wWZH#|TQ;rQ zbe-vD4}nq^M5oS|x69k7p`h^}3-NTWG*0NGk!ac3|NigbJpfebgf@hjCKf=akTt2}x=E>%F(}4}3D&g5Ba)rL z$$FA6PF7i-qzuua8yH|m(Y1d{e7ipWVO$R#FgW8MG|X+Cll(Ajng7a9_C=k8Acb@J z5-(&8R!1a;4^2I7tFycI+-)?}v1FS)xgqtlu7X`XFC*yB20v?C^%xaxYc=@6PN$UV zMnK`u0X6thbJCp}xnKFU58y9;)A!=*{_pR@;~#ip8&bE%Nnr&bwKb045IPS)30!1c z`K{s- zn3r3RNMbQ}pkq_m)UIo^kL4xd+rqxoBrLzLy@R4 zWV6CdBNGdq^{zAWWv1Kh*z}bWp4`gvcHY_E5_%#c?I~fa67nF3k(xh!aGABEAdSx? z=3^trn^i7&!Q(}n{g#--QGO9P&p1+&?5rg3PBZc(_PsvEawD1bX#-KV%wtYT64V~( z6`N^pQf^qE@z@2Q_xgVXfBX%<70>(7XR2YGO+(rIL+wcKcdNb27q1%X>Mi{Hc-sZD zi~plC;V|^Rd*1z)AcY_BvCPK=03QpU-Z{9E;}dhfYg zviaP;iF($zObt!uMe@9lDM%o-k=JG+W;;r>F{)A7V$!Oj&{u4j&Q@?~i`}J?fTfhI^TE=Bh+hLxI z&x0Cafi@dTVnz)Z2|!1mlX|s(i2zB-t&a+);ctd|2C z=Pu8l@Topol56jbu9DPr{C=s5lLv6`)=ytMQw&~zyucm;D z)8g0V;>oi0Yl8r+ZM;VUn~gDO6+(2HP|sW&ngj%W~g6FJ(**f;QGa-}AFvZqmc-x4EwR^Mro}84!9! z$JxG7Yj^G6XaQ*lr${CPCiP7~k24 z^xNPRnytbm|4VU>b*!-igxWCyR0jj}9%}F({<2mfxCOgusv#`e;Q1b{Ps6Ff-J)X1bVO9nXUWp%qEb%A)8DJoXUVu zV_J&C**^mPS@GHg4EY~MYAQ$2W>3a|oV*~@S}eDjHSg7*%d#eF7SL|E&Rf3kXYk+t z)py|ge(dK;Vi^Hd^0;Di;!(dE0ab6jt%_rixF-AA`VA*KH`U3JUO5QM4uWZ9@RvkV z=(v8Dz-4NG6{^973fn84y5Cft<)1oo=h)dyDBqNSH}Id-Dfd&1?8%Dt{_o_AxFrt? zOi$_g3iiU|ivBw}4Q|$*y%c>XQ;S@YpvQUbL`y@KQZ4TKDrU2%rf=@2@dk+HJ9It$8!n|l>RN_BqwS#fnR*Y;w zIfrzTI4Nex1aC&+`WiTNlR~DWyb|OUFL@#U<$vZA@#(MrD6d;=EM2Er{oXzXS4LAm z+uU&PYP%{(Q*3FLd5Nu3zEI=;diC+lRD0z3kiHO%{NCU{)<2a`?>qOG%SJ=$XVeZ* z3poHxbWhGh+a+P;K_+fn@Xh`KDSpCCIPt(qGpI=KxNb*Su~FpzI%bpsfsXi0aPkjF zCV4qxOXL5H3dh-hYufsxa;zUrMKpBNO94Ht76=5WP5_gfG9m1sFaai2r?vq41fUTp zWF<~z*qE5m=WZA{?z&ax=}PQ# z9DKwd)p$Kw{Pnl;?-u{FfIoB~sJ9qNtG6&!H`&@IDmMcpRGGB)Eok62%I}QgF2H(H z>{{;!sI3HCLBDrCa!)l=M4!2<&VHE9O!P?j zcm8xR{~H_aIJvV^0n8vz;`Pz~3l;>CHQ=;B$X%9yu4|5y2xk2-tlgzBM)&qksLVlf z3bqH8$+R{#VoXu*=!Zq%A4;rS5b3j_`A) zW7}q&B4FLF*=p-2c4m*b0K1(FCAVRZV*|S&vOQmoS{dRkVn_RICygX=q+Pn-axFfI z_%#8iL-kv^BHc9t7+*~{ z)_n}P*s}F2=9ZHyZF(4V;A0O5=(x&+v&_iy-UEI@1sz41=x_L6Hp_FkItKQPzkGP$ z(i8XwFKYZlgEb>yLxNEj=w|c2`Cxov6@0<-J_Nt-cfJ~5`dJ^h>Tm&J)hzr!m6dw8 zV#*EdQ;C1MjXdaLeYnhTTC{=6p&cui6LxK@v;Xs4FVTSC*LYaNr#?obSYY6QKXt?) z3oxDkZ#pcFn1JPm&Htr%joUQ)YCy7ISNqHMuO_kGDgbMtPj+ja9P23o6Qj`4k6t)Q z8vjpib_3ttz0px(+$R3zn1=zKjR2^JWB)G^fHklMQ~RiK6;np@j;_rJe>ODJVwY*h z0Gzdpr%iqK^`6 za6cPQLBpFKNtO~6slRRoLNvv)aSNq0?V{f#UM^ZG@C4kp6#OY@5n7YOE`F2YG(Z}M z8_W25^g4Qqd?b6g(8&JJ5|5zi7?7p)n5)gpp32U<&c}Xinz&oga3R2A!@AY>1Ss)8 z{f!d`^30Cov*~h4v-8dq0E1@HjhY&8?lwS6x*Pnb(CFgJy~l_n3MFA1n;(+Ig)NFz z3ah`EwPpBB+Cr`jHnx>{<-@w)$1;u)GxS>@>WK_7d(Mg1YWuSL-2G8<8aSp^2gV#T z4*blHhIAns;6#`QL}K{G2C4LO2+@!9nS-y=5cyxFZDTx**LVfQv>>ZGQRjnw3ZZcG zV5|+;q;Lg5=wA%#=-|ZeR4bbQOGY5*v*hEsAMz}`;h*|={0pD|Dm?pHkM%Y)Uu>Ja zca~cuIplaE$<2V&Ly}{+x4F$GE??*;-&6!(xUH7DBd`1vCbVtv`|2fo-^2+VlQXcZ zB<}oN`5P`)-id5d;q1Ete*vnAx2VH{zdH6vPD3`s|D|YWx5>{u7E5%ii7-mN?sU~B z001BWNklgCObj1W^dQGI!U${=*Z6-#xtW>)qCA)loWr28|0Txm>BGr; zmO$5sp@{0r#VPnz{BQGrfd5Ht89ihZz-;J{33AnrV^7}5SI41tbpq|S>VY^c&8@VQ)TOgd|T z5JqTwVX&(KD?C7`F1Jt6YoCIDidm5E$u@EM>{wVoSZB`YhhB&iVwx1N8%r#v3)Cqm z`CS`PyP7?CeZDvRI>+&gzw$WV^w+-!Z~lM2ACEr~L0utgy3}sPvv8ovrn(JF24zrt zMQ&@-ELF2Jb^+kR0Y%7vyc$Fwk%!cu2+vVB1NI{R#C}hHPx=oIEVOPS0J0Znhib6w zu878glgNL zk0o<{59po(e54$In?HdFm<`V0lyP&?41Bv%20I|;v*M@GlW0D4rl0Md4b)hxR;M19 zXY#%vy3QCpSUG#=cc%9*M*g~CzmWXXxAs1Rk2F9u_X+^Bne!^RiXUPaz={8q0|YQ) zrW^>HO#ADB2RaND8>I1jqK9Z5+przM^Qi$=!&fpa;?pvJLyUk{*p%>N+Lu<5)+9T|jRV-#UqdY82H0 z6nfhGNjh_i-;^!^UG)_-aLlw|_U{)ZzevxItUScOj;nP<&=P(dpnKhKwH=TQa#`hx3eOrqw++`Ar>h$GvXj(%uq&iBlBuG7upuPbCl z`|9`)G3q?H#0E>`EJwc7=Q+bMimZE`t&aMgb7d!yV}fv=mFS(5Q5&AT$w-hAS+kB5 z+KllU(*YQS{(gEmF~Tf`MI zYoR0)BPaP0eGETNZuObJv%b)7>#}3~pn(KCqAq>kjxqe0ylKh1VYvYHW#WFWJq9r2 zm^(L&Zmc$`24Zu(V*gF)S6fRCoA(=!6aFsJMrD?N(d|5AY<~eiP;Q$3PiG4z;`{VV z4Ajl3=hCyyvcHz}+i~&xlt5o! zh1ANU#=mY;LA2gIl7k^W1VT6Cf2vm)GW%GoH~@Oy`5B#1;CFXSd6+4w&vk2qHwHQy z`&S#C4Sp*V_&8I3(}`ObIBli1JSKZ_NzuJ9dc}NEBMd&ZS6nK@}Fr`SI%@aBfET*e_A3hK zacaP|0i)+pFB@EDSU4qvA{(tX$pfN|-B1qDwe38J&?mb2It?yl_)A$dMyzS`-qn?T z)B%DI)_}sjfsGo-jh41I`Hz^Xb~#FpS1x%M3Ug}!;%0l=_=a`=CWX~!h8M{K^;q%O za&G5$gDOmJDAC64SkP9tMC4? z_ux(6_)dK5cmHIdksTxnVl~c6^g+*ZBHRu5={1Dr>hr6f@HK(@yciOtr`q~DVy)y# z@}C2egh#fWivKEe)c@RPa=4=?0HH_Jnyf5M2$y}`+}=%nQ~0G!*0Y^v`9_uh41f3A zfXde3h|`39*Keh-{#|(l1{&O~2kz937P8qM+2V;d#1pg{4voMm)-n6|1Bp8n5oN7Q?7{GjD5u5q$ z=eG~|AO;Y--P`Yhs*?b%1fXOm;+xQ!pktC1ORPgm8XQ7U=5_Z^XYx(wf38We4*1Nx zC+ak~vXWm~AyJn0_x|7T_f;*!_S2g#d;a_yyZHx`>9YxhuVE~fpI)@HL}XB{>t4A~ zs;c{oPgGd{ZC?Zbz3Q!fHMT5Z;x#XSG5**aJ`w-K%RjtjkBD1$x&6uZ+_W^V&W`06 zO3J$pina>>tsGSTbNo*Mu>Rh3YZ8E3PVkO8oZLSI*U|=*wAex)0? zJ)n0WmKz_k;M4uw&d_rla6_`fYJUR)Au| zX%OMmAewjCTvEK6pA~-Fy^ANuKj-*2>y8epBygGk#i>O})vrhMe`5cq;$${47x&m9 zJ^_FVAN}Sax)_IDwjf**GEjZC36ozsA=;oh1!d+ax_}@bk4>~sN)dslQUqaqJbC(aoQJm|JEP; zdHne|zXRX-BR`|YT?{Bwa6M;dOk|6`#jtamM1P?bg6@ZQ`5MBLRj~F0$6vK4c&etl zv17UWQ!*rBq*>+=MdZHil-`J)>&q?ksm)MXE*vrxv zk<;$)dbJy>XF~3o?To(!$VmX==)Tk(G` z)8l_I-1I)vy{A9xWVPF!dlS^`z4-s0HQn!H-4g!-%w&(-#Q#z2+3+IDS^OIIKiecE zS~Us0`;Cw#hmGzPXMR3z*PRf(Q(~Jer9RG1Sg(_BWxGu#H@n)i{Hx@s{jURN%m=Ou zWV$8=7yM6aEKKIPRVY%$|FLW{0hpF-Bc|smXlnJK`=6l&1Azfp#|75vqz=am(KzZxb__=$aaaA3}fw|xK4;w#_$4t(#A{(Q2J z>L^=gu#=tIha`$4llD%VR&xw7^F1BSK9evV>_TUE!xr7JcLynue#lWM42?r0nJXjr zgh#bKH{>YsL4I>z#d&V6E$Xjub%T=XJ9-ur{ZD)heE~s+GhX{I?=-F-+E1(x%&#v0 z4v#PQ9@@radp9s`b2Hl%&Nua&LN^AD`NeWm16o^l&T?oo*OhtImOB9gAK6Xc_M4x8 z$Q2N08*W+J$Hw`4pYfit(|p#4O`pJjb>K6)gJ%7~n=aB$^&?J07yy0#pz?)**f1}> zYP8YuH(ojZ#&V|R7`}wvc)>Px3|%76wqw#vw+Wa*F-ARpQ0#Tu#UJ3A`|UTk>9(c* z+|HGj-B*9MN(QW^04DMacC)K->cDQ>{SXbplpQm->FN^n)71`2BlNVi`Jnh0(xwT`pmuxx*S}W&h&w{YS{?DStuQFg zsf@;VeUx+hOOVxj!9V+!?ElcvCMp*8pJKF;-v?wBfATxjR@k>}@Az-Vw|G*LH;r2i z7zfYZ6*~>0j}8=^gOT)}8Dm!}&jaD&TZ_G?I7zVLZnHlpyLF(YwNaD!065co29P8N z!3{yC&`8U3LeK9s9z3hw7+5O+j-!H5r*lgDZza`+K{ueliFEXtf?Fny7#(Q2ro0Hr z;W{V&_mlaDCh-p`cWWTzQTuz8>rwI7Kv`v>gZDoFn}k0_^EHS(c-79awBwt*Xg0ngoai7lq5ua+(xLXImigXuw=rQvtKrW$*WW-G`EV#qT?3&j zuLA;Bz4}=i5NPZq2JpAv@l*KkzTvy^!$0+&fWY5(910?q!9F2%PWHkX_xhN4$oF1C z2T-hCuYGEp9YNzkMQsY5lYH*NQ}CxgZ#r{*p>c`YFncKjDp3=yNAKXjLtN)7~ z{g*uKBk^zruJ9w_0{_spfgv04`8U|Zo&!2C5&ZUz+C&GPIHH%+W}*durc=T{jX5m* zN30lkYVXe~rXOU7`1Il`bL}+r;C&731pjjJWAHE*1#R{^P9uN^%fYR z9?Q^G2IU?Y|1th;{G055v0qJ`r@l6fXC8pU>30B3I5*|#?xW-u9ZgL!z;^y)`0jevUAY}_4rsrOVx}uBy z9mju1#_>Jk*-`$P-qfkMlLtJvKAmpV{v-g|8gfh*&ly*x@GPCy@+1HRTLyf$W{(;= zu6N14`Fnbv6d?nF{~>e6u|N5DFxI>cH{j$lxutt0K+V+N4)=-^1$i3Ii1 zh-m?hXCtC~prND79{iE9jK-NR_btF_@VB>r2hf5_n3$fVM4KxyTXip?Z>FOco5kOCJW<)W`3!7b z+Fb|j={Eu2#EhOaxvsHj+6*xJa5;)Dbdv}G3AP?$04D%Hn|{|J{MYN%#`SvLN&MQ3 z%{De104({x0#1AGexs|_Zw5VirhnC)Wv_$0cS6We;>Vou)f0{IgsBdG`RqE|Sbx82 z@uvuJ`Tz~u?2`!Mh(u-4O&^zB%bZsoXc{Viw#npzCyr*c4G(1!3CUlu{n&K#BOwz{ z*8A9-g2r?r;m4rgM*Az192hb9Bx>OLw4~#uFM1xn{qV|=8cO|7f z&%Wf=6|mF+T(;*6#y7FSq9u`$DViimjI9}KfwM8Zt9cU{D){G6EaMDmQ{ye6MlZaAURL? zw7*#w43&;K9f47wxvpkAge3Euo|6BO1n3>ZejbJ|CnTzL42Q{8RZ5^W zjwMRXrkS2m`8sNfRL^SHM831t{NA4Dd+(*O{f;ToMA6R<7fHx(p`RT~XKkzJtE0rp za7jz7uVFUho@Qsv7?Fdpwgc5P>uPOWR|EyxsnWna`|WOP^^5c20qLZD@yR3y_Tlc~g2`T~uw|p3&a@ zSsFffV&}};gfse>O#xy?#Qt74joj~J$anA8-G<0B_`7{N{eT8uSYV3(GnwF(5#7p% z37^q78cf8|f9|hLW(Iq_q$MSU=Bsi|*a+;`5QreKr4q9bk^-l4B(*K16(~_Q*SncqR>Mr%j zHYW6wiFL~|SKpCiklSsXa)qZ90Cwe5Y@q&BZMg#svVRBrFMeRt*~%CHZL}S!Pmz^w zNZ`;lq$%ZE3Me=h?B2HlKz@Fs&Q|C6UF;v}76BUmQ{&%GnRSJLJqe1{l@HNIshi&G zGws8XyHO`6{Y(cy>0>YVCV+!vA~`mucX)3#6zTq)B&Yh>@Nm*eh9S4z&2l&$yssOE zuTglUdiRXQ`TkzgujQYv>sCM6@N2y~7?9rsw0eNjnw3Omy|;0RE4+{v`f~Z?;bZ%n>ACa#EBb)JbFwN71h| zxP9e>larggJZx~=pgwY7ReKSB=i7`6p{KG-0xkF=7j)qKCIuz_;;|e{m=4` zWZOvmdjarPO5eE}ljL|$@-6-k(~}Lxe;vDbX^t3MeOucTi0_ao(JBePm07j-5oLzq zfE|U$w#sODr5X42=>Xw>wWJHMqc8ISYvx$C^D$6DpCz<$0E_q7eZiOE+fjTbkD))C zR+LDVS?L)wzRutM-Zw=|{&S4Ic_AnIm>Q!PlSa~KL&V|TeNFu=W8#VnF3zt|Tk2~A z!Y4nh{vzowdP)Nc3$V(6dq0Ub*~ZKn{98vxVy!f|Y@W|#|L2T-E=KnMPD)HVj*?ez zSm|}lp5iYXC^}l!`BdbRo#YvR=D+C>0DSa|pN}v3tWUu2`Se%dvByUED^R7zu9dhU zmAjSnXUkKyNfWiQ5`g+H@$dgn-JAdJcUMT<5G5GP36Y<3*QvFpT=CEK!Pi4rAB+{B%dDDs{DaOa%! zJkL4vzMn5e8R#YW{oHr%%$c(EA&`SRUCnROhY9_Rh|v7E z+6(fE8Q^gxbRr20-d}KkocR{TU4f+I2r1-%$?X&yc&ACg$ zZ?))0Ufd+Hzv-M8`!{&JQFC49o%pijq9o@TC|fSg+%q913IUJyj1ivPY;+Hbc1J2 zU?_JFXrbkf%OsddFrU(ogE8nz7kFCUnElQd9=HGDpM2i_;<0DU1Gsm>ov#F1btiRD z{Ga2#u1|wLmcOfEtFEK2ZTubj`TVE#%SZ#;)2RF;`6NpbO0q}{qBF;#!tgGs*QH2; zzEKLeFhe=j3UU<98j5!TihHtk=usE#*LW2RRLcza8(p!EuYdLD6;yF!W(x{G?xs zI}z4+-;YR z@lwE0DC45v;T^QZ=yU3gEu9JX?K-#c#6_UvqQ6@}Z+}lcKkgDiKjp9{_!YNDzw|i(md;f+Ge`m>b?L(9j>N3P_@#pU?5Q2m0gq{GXY9;NADy zZ~V=V+pmB6;WmGvIlDQU^WCog?zlDo$9=+=N3`b1?T9PI z;5jEttcT5b&R3Wxm`AnZSo4vcYko!j0kGy^1I|5;H}n$*nwx6^I7HqavoL=^<&(I! zUlgguefl77S6IYg=tJio$MGELFG+;Jg=<&qy(Zt_MG-9aIZk5S_Bq?4KiZA1P5FCF zkGwGITWJKOtF{^C3l4p{y#~T)5YPuq%dEAvn8nHDmjEJn(@B&S`slnE)3jm1h6g$! z4&u5CU;*2&yWw_yv_Ok7W&{_nwMB26NY~?1zYdpcM`s&18uvQ!yhK0B^=Nbb(V}1b z?d6X!=y1nosKL9aN&_v;*`zNRPiY;S_QX?tbrS>nehtvh+%ZZe`JmGneULv0t+YcM zj8AvSl;Uc6*A8y9ae7mDl5L7!2LnaRq+$Y7Ne)z1MJgHFF~9d`Puc(YPrksP4CTdhBV!utVk9h;LQ`rG zA6y7Ql91oL17^fa^#c%P7^j)~Sb`MB?2)hv4HtttzG#lS9mVPdwOF`Li)XZH3M?uY zL*_iM=g=qcZAQ`P*qrC_B>~?p^&)y&##5W{{s1$&b^0j6^KpPVCp<^rYy<=kFD$5= znfY7uNBW0&b{*diH;rB%=gdT2p&XAw(&91u+#%yc_>g{Mt7bI+LDwp(DgT-A;($5x zZUi@8(k=5vqU0BAX3pz(RYaWJ(2u(3UwBBT>-s!XZ;C&gp@V6^#{}^{1kek=o8%0O z0F&;N7y(m{8vhrq82^?%`l1ou@qk4|&`+O1l(>hwcFsFLYEhMMei&oe#~!-R{<~lP zxc$mcKLp5UjJp_jZt=Ku7r?HKBRTkgkaros$$xX4W!%j{2>|kK(yi7>@0bEigd(K# z3qzRO#Tz`&yrC68UspF@;41O=fYY)<^2D zEhR3LXdo6SYro#_1S1S(d|6a5Bbbdy&CKjGfBK~TPyg(X?T;UMDsd_iNIfU(9m?Gk z?Lt37qmw0Hlp%(4kw5FsX5o?MQrww;xk8;Zb#sMWK+aBknY%H7pwY>T!|}tG#OWsV zvmUZAIzQj?$Ae|G5lc*WUjpU$r_+<1Id+QYw0y1qf%iFXCtrMCh5E1q)Onhn;Em~L z&==<`E)dX8&cH>1Rh$8BrxO%gtB^XKash$g;C>Y5r*Wb;-w!i-$y@9QlmF*JEB)Ju z0xk4R-liYVii+Z zyv?6olJhQvy<~L;`tg%{Bk=(rqCY0N^u!{#O3*1HNz+M551d9<2FKC;I#!mp2FfbDr&! zAAYy}{a^iY`}D`(4Y^~0;zMIe7h|N4`XD@X=P-2_`N%o>wfwj9f1%MHd*C|qt>ph0 zLqvWV|D>P8qXekqCFp{*jn#^UuJSU^8=Ey2jb791Y5!H6B}F|F001BWNkleyE)-aI9cXQL|21cTm`yN}ES^gpUni7} z9VPwnTl_!5>UQTHX7T@!jK;On#suh{4=4v+po??Lagx9;|4lnO8e91n69fI942n)T z{HIU$I{>g4qt$Lo!UyPabr7AS*ew*4+*A$lg0o})!t)e(O)_|C(W0~G76*XG^Y(@G zO|QSv$F+qLv${iZ@vpIbxHwlwXA~${k2b|^>oF`~A>0yNv~me346n)<0zX}22iNNB zb@HXZ6g4*@ALZn<;OA*7!F%B%(2!R}%!Rkoyjv9vCTy=oJKM zm`CF_I{T`OK1I*4c0s_B&$KX5*&MQG`Z=iY=_Zfe(9iM@J9d0F$*}S!=;`!d{U6A= z(6Ro~=i36YERf=M?kM15Ajt&bJ55gWw7f+c6Ou7_w?6D~?TF)B@FRxXSOq*3>Z|CC zx82Jv+P%U0Q3dPYWY1z2^$X?IfN}aGj;xEp8!sZ^D+)Gj!gKVfr)D$n5?Hv5VQ}XI z7yMNCL;mBBKwOJjW2wL~{zQ*_47jab7sDlCVcT`R^7BKuJZ{O$-xm=t7xXV8tZR#N z@v=YH6Tc$^<%SEScvpntow~Jr&!XQSQ8{+{^OK9iEFOW-1!Z5HI^=ZBYammb%T`WY zkj^u}D|S5Xr#|+e{r$i7ar>E%zN^v^etZ*8E*z~}bN6codU$g@B zagsJpi27dm+s%yv>*g8&+b*16I&P&%zts2j6#K7t0bxtC^?~FjIB4E+Ne{Yl-QqwL z{iUty3LoNC#DK6s)qs(7p$~72PWsg#v|A|X$Ia4^KdYbb=!e-gsNnVRum-$P&zp{~ z_rkk=%b@`LYzvouitSuhZvLdx#b`5g$c;AI9o_DkK*d8@x` zO5C|0`n%qNAn@vL|B`>mN&Ln+@1|k-C%$jG<2L)HpZuWx_rLsO_L2A9LotU9Kn{sh zZ9i>uJX#M^eM?#R2&?C~%JEY3-<|%~!e9L$+h(6{)CB>FCB>J|O8v%J$AYB+eZs4Y z;U7a4qJ(2_?Em`sh`x2=#G@`Eu>5bxEB(7>-4(1_s4{OjCiC4Uv;4(ts+C8gQ-0MQtYB{sW)^4ih$-NEU8li{x5v76)d zEF|2FhSt~IaO|bU2Zr0yZ;i}1<8CJZ+BfFv9Df|Uh>DkXF#6EIJJv>~Y&rs+n2y6~ zHvuM4v#Q=5j69LSSIDnylCg-Kcwa64i_;|0znNvb&h2G z6*zi8JEM^H|J;kO*gyEpBlZt|`_Ju}7rx6u!;GDDcL48;`~{gU*;|KDqiFhRt2Q_W zkLu{B;DPgYcEvc99MLxNN7@PnsCUCc&=={~HoS_9jMB47;A_lFPhn<-pDY0m`;JB4 z4t+Qb-}QB*A}Lw1GM-&W;8oOx{JQu83j-5g6lb6pk+|^7@%VfI56?2O%D=Yf7)L($ zoQW2*aw9}h7H_;lfB3bK7ueGch(j{g+CegcmElm@)8pXX2;S&w8mzbS{4 z$5?`grZ0tDlWcQ?1A(+K5&z7^Ae+e_l)sO-`_4DpfBxx@*x&n=kK4U>-O){xv}xjf z>9|Rs=AZ@InLfN#|1As)zWeWi@JOEq>1goXjCjAx|Er^K&i$aa;YbzScVW98dnR8? zBM8DKe@Gt&@oWH$3khJ72Ee0nG&b&2*W$iVP>2ycjLTCKczkyt$JrfsFD?c&%-$^+ z+~{|lav{G}gwFrr*hP%`dE^!@EL_TenX6zg=oq6<E>0w;kY({n8-i+1T^S7q^4H1PO z>emC^Bu#v>&?Z3!7r}x3+E0Evt>69rYxbM}{;T%C{p&B;V^6(MowWZX2e7V75~T8P z{U3O1a^BrM4q5ChD-H`D7yHM#3>ud56@JCE4Fd=V!D}b~(4|5QXZ${@=UwqW;Q7w z57sz}XFC=cTpl>ioTaN^8Y7>1Uqw~ku3r7T${=nN_cjPh^k@X>@=Q3MoxltB%8pe$ z({1bAj2;89nH(7KMS*#aD<@C@^{}n#~R)=CsU7xzM*(ON@r(-jV z2K*S(yrX|zL_eu8lOIP9s(Szi&pxzU?=mVuU;=M2DA=aG$}H*XSFCHmVSX{0 zoP6yPtlX_=#-VUberx1h*WH;Coy0REUifi7Py1(o@OAq?|IL@|i(mUT1tFgInyrIg zH@KHTqX}1M*AVI0R|nHXwW{A_`D1Vxy#Zkq0tOVi3~@1$tA#-N0K1{zbsl*Ec#>_B z1E}~C&yY~iAGJZ%P2I)AY4e-rhdqqpcWoa)eA6~cLb#5FP9U+0-LoBVZRT^+KDWP+ z@2G*mPY*k8F<;L~`^)r>7wdU}xW>Rs=g3zh{qS@^5%uavk2EX>;39x0xmX-bnBR;0 z^?-UX$e(GqIoC@Jm+u*Q>w6Z&D35$h{=5|BWI(@}O@Hov=0c9&9e{RpI^_;~n3{jJ zG58{PJ-GdQp^tX4x+dL|Ztusp-uoiNA%E8`kpFr-1QWmTe_}=G@)LHy5K?r1lY_qnJC|4#d_f9YfPYd`mausw}K zrtLN4^|55vE^S)8V4I(3goXtU;lCa=|Ap8qIk)ER;qxW;ZjwD=knwN*>(?BH)sP-- zYs{7KPels$BzO|0H2h_3<-Ua%T{2i1&;0DJ5#hq03?3QBR(maQ8y?d*)FJnOY1ewk z3@#!%iastDzC|8--o-zIYY0R83o?skn1eZ)l3O)@c@aSDpZrTL+ArFk;TXQINM!#c z7GVfcY5-11;S2lho&iN&9R|vB3h2Ro(PaAt0EcerB)Vcgj*Vlf3k4}?9HWa4CheCB zRBZ2l?`-;#tydT~EUazsxc@SboA|U#{PlCQ8_M7KcF+RX5jieyoMLtM{qJiLnu}C4 zW$eEVP{2b(7Xu3g8ADyJW1^c*dX@Ys9ZWh8fTexANXTPrf+B@HBp0}QTLM6!BKiRi zH$#<>79?TP((tNl)4s&Jp3lYE8XxSR7cIb}&wuq9``>@-OZGc|^bPyo_g|waqmVHN zvN;<0AwTQNz1p%USDnDZj~9Iexr0p0$~m$QZ5abFb=u<)-PHV@{F~4ZIE{|as8E-T zdY4r|XAa~&KT)@wH)B}d`V235vw-Zj)B6eEM0QN#(`)N5wnZ{0h42CM#Gllg#&1$z ze|<;R)@|g|KPkA*bB<$-@ASG1GKoKm+QX)D*O=Z?9||zd%z1M#lJIig8s9dK1@B4a zKaJ@Jp5a13dFdRyjL#Qp%;}tUS2(hwI08Hk(oXBJD zp;q{%g;OpB`JEDtPt3atFy^Jr;@1vBpO*Y{hu`JD^xt{5=su0~>y%$kT@+D1nY+T59zRLI;Z@?(S@wg@!fOUB@5?tk!Tw#D8o5(XB zTlsMyxQ+kB7jmSDj3CAf1pp6|&qP}2@6Bka9opnvc5pugU;dh1c?ZB`hT?B5dUky- zJ7IQe!KPO55b#RBJuK~q#@zMU!T8HOy4=2{%iQPO0{!SFgqPmNMI6sT@`wHkzs_g1 zE%Zk{adqdH0&9lCzF*)hj2qI2eE3M13S(%0X(KX9Ok^tz`WJBIEs&o;u7t?CJFkuo zujDUXjW#oaqj1~RYfr4!SYJR$`ne4YfFxHJ7J+m;w7u&m!6CWEzs+@c2^7zZk6*{L zFMZGc(PzGD|M1`bg+2A`%N&#rVz2uVkbl}*!w1Gz6ipw|b zAMoN+cWZduDsAwlfA}<-Cl+4xPRm=rb>rAfZ)x}OX6bYM<#lHMk~=@|o+n59N16J| z2BVKZHn5T=Exz3T{bhOv_&OZ#>*EaR4_N%E!-0j6+4fU*)`p@K2EB>bH%(bo{xJ0JiF z$1xU6_cKI)YC!bXHi;8y9e-k}a!gm`xnlqQb$S{S! zna>N?;0?4nL;Hm(i;z{s386^yG~#D6j$#+`GR|GsI$@f?P`LP;0s#7oCTEXW3;YNh zPF#^s`3vJ;7Zdlil=v_4uT^o8N6#af82vuAKPBPz(~H@eH8Q~ zTPt~h_HT&8qzU|Sf>HZlanxfS`Ea+F3LZdh_`5Mj?-mOHl8`!bM{jl(UEzIntJmEL zp;JS`(V^XZaX{Jw1~TJ%A+I>p>kV_8YnNc(6qlPE%B$V=o6gzuoOx)rk*fwC=*LmG z*P;ICqPNL!Ehn9|R6AOX-BzFzyR*jP-jmPW)x+4HY)gpDu zx%;j{(P(62L^0#g;x^R+5{)?P{}0U)!8((6~XLt^G?VjYTlO zHi!N5&pu}V$8Y_)ef}#?H9j8o?2;|A-d9}&nUeggebvCC3_!V%U?h1^`f&>J!Gwcl zfF?o?C~W`26Oh02&65$NY3u(1Vg^mnf0<5`k8%h=+gPxFnD^MUMAIvykOF$7U>c;kL7NCXd!_~tT<1*Nr^6}-0f z8jF}JzfxfmXFA`G3pKi+;bM!&3eIPH_`!GBfA!aX)PDWvKVY|?PT&{P9UW}JqPB@a z-GMIiChg7jOGUn3$KPeg7yESi+Hv7>PNsew#2Oz zkHko$u}s@8{5wA}W|cz{!0keUCWiFeD zxnKa=C*_5H&J%~iJ8*48zZk%r<_iGHK$Q>$qhM%VIYa>ka0+l*K$^f}TYS9>0vybF z4jre*>nYlQ7=`{0ez|7MEAOw#Ot;vA$)3)IiO){{ou2e_qi%V34;0I=V$N$3)|x(S z5OsID(?w1O!U=0k8yGC|H>1il7u_=$(~_@&C9s4B^1_UOI7AH&$p;NKNb=1m*?tqUTX>%m1R{3!^r^Vc!EATey%7ry$e{jdM(i}r87 z@J;)^KOTS%y-T)2A7r-$WJ|%-GRPqQ>ELT}a52G&xxq$5uqoF{e`R&djhWfj6*3f! z3jqLdNpkwd$V~QnJP{R+waKT{6M!;gkQvFa_y5D9*h?i$T?CmN1^=+~SzgA5d;$Kp zFQr?>=xN>leVmWDyC4cjR}{Egp%itVmCFwB#Zsmmkf@9x0h!- z@=|@<3FJM2{xJ?&(%bsor9k{Ufb%W$grg`)EC%ETKPk|(eAEN!&tl(kJ%jJe*mUq- zUbN^kigzfCXv@19Q2+B2fId$f|6J!{@#3K0b@RCU0KQ`2_F)$9LZHPDiId0Nr;ruu zFWQ0Q?4NHubn{HUY*N2N@9wz$Hv5H7z27vQWn7d0_y0$C$B>Xx@FT5A!$6QQKtu%T zk`O70fxt$HbW6u50YyQib95+@(lL7U$gyDCpYQ+i_}#hgzONgv>%6aX&g(o6anTl+ z9eS#LnF^v^I&b$i_x=p|(p#?n(%$yxC4uVp-@o;tj^O$svCx!icFT{2yutL5-0SKb zlTr_lru$W!+_h2p8cJ77pNynXVP@IjFB;BPj`*>;h_&il3KX&)WE8J<7P+`ZoQGH> zF~J(sA&sObzshP?X?U>{7oX-2BdqfP?>;tg-06dR`V_qjz_2=S%`&I<0%Y#OvN_(E z=+#)lc>rW~6Lc{ibgxA2-Tc|{8O8`%C{LCQK#S=QD&f_=hUE31#}?~R+)iAnunt;M zDm{|vPE|%nfsXOigI3g*uN>KE96O|DT?9zF0?SJtaLYb2tgCa+6Bw5Il!kb?nHl|o zHX!86@)|Z6cC+mUtQo3u&uA~=bfD=zbR6N^rNBe+-Kk=1;tugecO3(J-~FwwXKK|{ zYK+WtGR9r~KpBmWPj~wHww;;g0z2cO;VwtH zDohqB!krV;zQu*Y;hn5@&b2&YO>jNcBk8fF^L^Np1RX7HE5^?GbWT+@B(hL49S z!pQpCxHsi^LfXsf5~q03U8+aFY_ToThj8qe##>b2VqQGi!9(%ZrucXYKvkmc(35k) zXM%|g)E1SGg8z!1FHoxUDu3BzuiW}FM%FwjNNYxd8-;~5vAyR&Mb28&_p;64ay zn{$%TjwiGNa@%;yuvAeF!ahtoQ{-VT;FA}~c{U=Go|INum33&f;5_LJTU!_q($|ma7RO z7gm7xOf8PYlAIXcOas!?MRL>f^V;17WUm6XP_ zpRE=QIRjsYw2Qg4(RwX0w}?@u{X@RLCIeUL8BTMhe)rP!$yYrX3&p<1N5inc2xu3< z2D46*MGpLfS9*K_!QRY}J9W?ivRpmKbeQy@jspw`Y`61DC{0m-S5B#<&#I?0I)f~lSE)DY0SF6%HT5(5I$ zPORb|h>yRn_{s#}=zO~xf3IEE^^>5;cMUJeklyVAn?UhG(x?&LhszK*QOGBIa+@L0 zKm5i%pjS3WEt058a9Pjmx4w>!GATTgk6#KXYgT}ckr&#)@c!=0i;cj~Zx-@A;eR1G zy%k3&5(sjDV`6FYtel6T>7*}X4z{M($HT#Tv4VTXUJ!5BMU3GAG#*>SS0#PV#}nxn zKK3@9a|b-7F>lvh|5vFz3rLkV>PJ`VE^hKePGo1$;Mu|xfUCpHVh}+)KkvVX7yN-~ zEqqtyIw>36+@l`ECs<3Vy$qXCm&OQTb4&d0=fKDCb&`Fq2h4d*bAbgtEE2ydMyC>v zd}&RcqJMabP^V~hXLl6h9!GUQt!^3l7yu9!`-YW?_d0z7U=jDo$l28KC#58gDj>7y z{SLuj15w+->U#dSvQFUiD4IUxt2GZGWkM(`PNq$>G{+|eV7=NCTGo^JQCa-&(~bDv z`ox)t6Mro`Js0Rp9?KQ0<1Gt2owi|HsyXVViPP_=U-QDhc3JjXcr70DFAJ6PxAJI` z_4-gnn^}v=v_7|sElYb|tn-61(>iSTsscSvkhYrKEWoBR*U4bGoKJ`2u57Q4+|kL< zONswI*n-bbkjZj8NG6KK&0Aqfx-wn){DMpYANO0muifF3Ua-Uiiv!zQpB(AHhE;BQ z1tNSLIz6uXI5>;ra9gcyl{C|Win(juW4fT6cqUhZNg!v^@QrQ)3S3_2d9|AsiWpAI`c*!~rUzIc7~r$K_Qt@ zQ$ug`_GDg_;7a!zkR_jGxpeTM^NR$AQH_h1q0#bya^hvA>H?bjDg}e=wZ!xYR;7>BATN`Z7ma zlACB4o$SCzjxnj}L6r$I5vx`b|BTNm=T%)3xPKHK$y4{4ULmHBZ3Utxul*ZOWZl>S z31+vUnj~bsuH|gaf9pRQH^PGoy0fKZ6s{y0sI#-MMMpv_GrnaRFL?+vtB>-21yQ|4 zEL%IyX=o;O<%~QMD-adjx3*%~efdu4MQkdBXW!Lzd%!uDBwRq(ST7oH|MtF|FPmWD zpbOwNEuP$*qo-B>Im4i5|1S>Qmwo3fAJP8#uBElIXbg8}WA{b0k5x&rjTz4CXR`x0 zwjYx8HaqZuJ67JFZgBq1)}fcDi|@V_fF@&azlA69WAXXo@iY=f`8v=y=L(?T{+z%z zHM8m%6QYjdM-r35TCY7z+nbftAFEy67(Te`zdCWu!D| zg4{IC^8KsEH2$JU^XcehjWaI%G6*u#iT_>2_VFd+aV}$$@&9=NKHkcGvHfgJo34%rU`WR&QpKh!LWvM$QR;M+JWgQwWl%3ukzZQXy62#=oRkclNZiq2-{ zYwASjK#et^#$T}u ze04Pi4-17(IQlD#!?Z#2=+DcCcWX96PQF=NDZ9v_pai}P=aXNZfszizYVSfdSpD7% zJREF&0O<%BC$(z)#oW?f_+u`gK7L&CERH3cGtG10&!rgL^?FfbyS59#IPLyM+x8!z zo(RhP<{M$Kwd#a58}0#J(rw3imc1`bb&AW7JVK8+!t4JBLJ%Pp{37sxKFBIX8p#fG zO&BPX{^UpV4L)G1e@g}QYCU?V>Hdc<|Hj2!vy7z!%tYTheTBrH|61#{=u>%79*l}D!!t=T!GW$ zIdKm>6%v-|jvvO{@c>jwegJrhjK+ulz0E&jFE2j+D8nYchg7le*gmM+W|ssQKv2{ng>Sq2;igu$|AHgO77O&j!hivzXaa zyIy2l@;I>0ZnsQtyyB*Xr1XD$at>^1k&9gq(9#yr_@NWYKRez0b5U*wYUZL_h<7Nn zaf}~zYKx@D)kgDvw3_w+jFgqv5)XEbp;%JhrW%BgbW=puc14h$d;@bPV!m`_JNeN+;;=O zx38|L&2Fl4q`xI8RrNPLh^OtZ{tk5~78zT=7XQ}fX^#JF|XK38Az3zzE-e*RA zEV3t8#JwfVuG)=qZ>)n_QX${CG2C6Pf5}Ca>c-91H8tL6*2*3q zez)8xDv?D@ZfOszRtS(d`>^x4tnhlfZfILS!2gvVzB|z}+!XhEs7_s4n%b%vt$WiF zBEV#}t2k8xDGz}ZXv!&mE6rG z>IND$FVT@yWCu??G*~qO4#O#pSt`Vel=RzCi@ibb5FOsGHpyY z*P2g#2)IuF3-cKKHbQfbGD%U_6nx;WcHqjfD97+RZK_{^mUXnL26T}|m_k7R5$93r z4n}Hc?3Cr3Y{O7wWZaL7?Fs|89+LSWR?HfZAIi31_I#p(*R)EPaOMtRpX&L>y z81~Ap0w1pON8)$?z5D6*y8>VSBWo__2QXJ)tOC!hJ{cFa6Er$^0i*Uhqgng{938tF zZ6h187iB=mVK{u8xlsUf84{oK3DbuTQ&RRnyqM9%EoA-!^lH23fAW@^TE*NGx+p2pL{`3g_}&w`Pk# z6_a*kj@cq^#EZV68JltFS>IJXg2WRhjUZjbBU^lo`c>ER=k$YhO|$PMwvM)?9-#*g zc0zYAq0dO}wc2BjOA4?j%j7JUIlx$f@xnvAz@(m~oF!$Vqxcphkz><8N5ray4Yg3QI{l8K@tYZJWL$ISeQe zy$mD+jdAGK$Fh+7ieG5YCd>|pFkvMKtY|f6Ukc@qJJ*WR4F2gv_lRL^MXuOLE4l_u zYbCwvbM#F5x9?Dc;#;}ZL?usp+Yk%4{Tf>vE91htt9d{}w*D9PzYB%wzh5WLyc+ae zp7!~{4ObI8mj5irCxn*U(d!=Y=PeqP)Dhbw`N;8`2}9|43<5{A1*<-CMJU=W4WtOy3 z{v5C+OXl#VzUMx`e=zotFCIdfOO!|&;okIgFll}C^!*RbB|JC$uyr6Bt9aw zXEts*Zf@Kdcdpvea9&jcDQ1|@8lB=Sw&RV35xZ}{BG`e}}d5_lAZg+y~+cK&bbcjFlL$ily7?D^*y=c!C ze7-{alTVuI2!)^jQDrTxRL8C85cgfp3fOECJ8!#s>pR__hWI0SgD7elgi-{wVC##q zQ0x4SeXQjI*-c@m45N&fbPD=p%6}}X?Z=(Y3|Deur{c>DFi0)obB`B!kw-ZGk1x9i2CxbML zqLSTg{t0?y?OrbK7Z5k@M=MBz`ztblvSKE4fKQTE{5Vw9X}BLGNYj?BdA0b;(GUuc zywjCm8;R;97J&$)TG?Mm_sEvByYSK|zrcQ%M^=i&pI!MathE@~zZZ&XhMgu|Gi{Wa zo{XxSA&Va3_`c|yKW*_XEDJSPBiaGTJd_ZL#$tSsW!LOIQ;hYN>lF#zIz8uo_`&S^ zhR;_x!4=nHDQ^MWIfm;%{U{c%F#oUQ%&45ySiqy6ytu9+|Awt&KC zEAQV_^;r8Taf^Lcqx=aS&~r`VCMLc=bh4j6cYHMDG3kd2#O8NnM*n&MB5bv>XWUH9dLNma;GR z=u$O=sY(>xUutT($~q18^{JOyB^wJhW@&7K>}0Q!O)~2K@(rOkO3vfFXtF3Hy%G4G z?CRw@mx!REQ$uZL#sLanH>V@=uK&jj+{Z5u2EW|mBtXpS zepdgphjVtQoKNL&= zC+@45)@f%4s&4>vcO&m&WhkHgExY$ZZddh~gQHQv3Yp2us$oI~A|u+lyfMNf=5vlZ zeo_K0L)vge*MKgW@Jm^+`dJ%QKG(-WyHVlu1EdEANzx%=OS+&Xaq|th?t;~h<39YG zT{?v1@-xp|yQlF#R+oFMXQ@|YMvOh2T^`VzdRfy#qE}34ZVZgz3)vyQDvs)8_xdu> z80f1~^g)vKdnKIr?6&7%QezI(g}qp2%P-9-pK4Qzs$`g1f8Q*e2QJ9GTo;kF2Xo_p87G#|qU z;!t*hkwy}rj!J5!SZ4lYs>J70MI`eS-d-?nEh?Flg&X2T!t&_G7Q`1V|oZrsOwOzH71@ zeQr|R_^(t-D8V& zzu_yei}}$=9jadA&D}P3abjTIYa13ZY*d&cVA-_V;7#={R1KT zl`(?-i?RE<>t5^+_VFq|IG>N7#2R_7o6u4JhCd-A1DZT$CF?A|+!8X`lFL0wl|4o5mBjy>t##9QD?C5Vf|7^{Pf}(M9ZQkN&n) z+QidVtbw`s$6^PYb_ehJ0VN1*l$oVCP%RWereKMpId0mKnG;vtQNB)Bg`F`1T+U>^ zxKh@A6s6TW!^xbOx_z(q?_&h|$|1Q9kTcmg3dn6gN5iRtt0N(j7k9*v>Lcg5x0BF_ zN0jC<3+=ASz58j4t;le{H^+Z)REMk0BACs!Y~V-O|!(hD>};!%#rGzrNr2K z<675?sKQiv4&2Eo3G8SF1Xa+2rqTdTgI4Yf)U19x{Psy>OcwEarsx5g1(f;gpZ=52 z&N_VWgyegq6z)5HAK&x+MaVN1!S~pRMbl9nziGQMK9;Z;37VfNgHnPZ7F+it9#qYG zf8dPDvC+2q?tZ2;R9sbeQtE%u^MZrtHmwxBj1>Kn)5PklxFCnUeST6T|H1hle=x2f zadvmG)7jLss+KabeN^>9^}|+`Xo9p1=MdQ;!{d=|xU$xph-OS7MoZEM;;Pu8!r`_`(O zO1M{Se3Sf4uV=!k=3-C>ZpEUd*~RJ=-QQyCE45M5Am-+*>nAK2U(QV$oMrbqj^y>u zt<=0H)LHcF<}*a#-Wxm;8YoG9HD79!?km_6M$xx|Jb$Q#MSi&p)pyNP%JyZDj`tj( z0?-;3co-y62i*D4mYs%%c`fQ;H=eA&Te#~KJwLMS#@DXdH8FpR_8IgWwR#Fz+_>gU zW8MnCUPtyIW;{{(nJUzsp$S$(w4S(2q_$okc+F*xaogH=PRY(ic#MI0FkiA$xo!tE zG}aFoRMx2Ak&${wfA^nM1wu}0tjc;QCvkxW@A1Av<9kJ6t_}EKIAoiqDD^4# zqTi&5{=|dlCB#cWbESSBrR*-vtrOny0<6F!0*Iq{CAWL=oXL z_Hte1!gXBP3)k;@0jexDA55gaF9_VJbvst-a(R&E!AV0Gx)VaU{lPE)wA9LF;8&9~ zF~{`8iiz|N@DwzWtC2Bg(!_-4un(JdUC@NjdlG(~ra3-4AyN8+1?+{a*=52&7W`RE z0=uwthN%XoD`=Y#0Lmh8;%js3xUnzURPL{!Bkkg+ZEJn^*)gY1ZfZWwdzw4GAYEE_ z8h~-@{0t?9=T8CqA|-h%A?n1~yk}SR0Ke$pR1A6tCJa~{&}Ay)sakadinwR)KOk^5 zTlf4}SHPn))Sl>>Wev2n;!e$OAn1tA9ISgKSLiS*ZCCSWzPJ@za|8{| zjtTwUbKL?=oBQIB=kCjA5mZ-=?;$N9m7r3zItBaf0GzdOmi~eIaU<&kjIbI}RU+DAq~UCVNUXs5cGWOss^5ccv2W zuRMmHT)s5~`cSHR@B`9547hYVR&SKJYpNx8Vq#z=y=^_l*Z9sv;VAQC?-}#`IppG* zh~=G5XNwgGg8vJ2ZC>_6tiT&F=My5KYd-o%2|qR?y_%T6Iocmxijub#*6(nl2~)k5 zpIfGOb`+5hVn9$RhWK@3H9qw(GZXaz__z(3-@lrm&1aepGQ0c3>vA!XSEd-)-*Rjl zhZR|pD$`W~`|Y?em9gevyMZMi;Gf)$kWmD)V?z64xF3y}YQcyGZk&MLO$NEbuS`&N zEbz<2{Lj1?P>N}m$#_E|NmVld`?$1mcER(#lzz2b8CsdRELAxR*M;QAf6($!P9`_0 z3#!B6E%JyR^5v>sdZMGPZe7a1ZFJbz@vdC$NzFKO=)VfbX z*0i|ZI{JO~u(CzTne^p!P;_SY)SSfUGpPHPY+<8=( zoeze)^NEWFW>6$Fukasg^{1=!3d!_)MkFd&uu7h}%tCyqT=D&QZxVl7yt>U2Y-j;&Yy zxvwPr9wV(ybV3MOJ(o}X!*ss%Ml9bZpD(d@i)e`@gC zvTKM>!E@h>;@0d#B~ku&qTxAR?S2J69~2kwo7LEkKp#}gV+FS?^-4EgZq%|Fy{n4) zF(s)xt~jKZ!t|H^#9?r{nf*IK<%L z3o}1^tr*GhorhQHLSu3qHFSb^My0BshHF9S1&gw~$?p}NA4d{GzLr%sqB!K?O zUYx&;@nq%Fj<&D$v(Xy&iLpUQ2y#kQZziYhMqgM{q}qF(Y9Gm1-rHwM~6?w-NiKl#+GRNNMx?>!kG-*0ZwS~nulYr z6c6icKP=hcR;qSQV!^!QDMaU_9@vHqmhENGtSVczII~>Yz-SR{{c+Zijq(y4$6O1# zUmftIe9awDwG1vSC{KSg$%ZIya;whkO)|O?FiI5+O|9k_;#w<(=Qaj3&H65VxJ<^g zwoBM0_-9cZ55QUX^9BNbyKi4-7uHJj%V*ft_&-7(ffuzD zHz&j5Z`8J-&a0%Fy&{aTIVTmYOiJZ2aY+N58praI6 zugBOiPqw*fC(3HqsXzgO-~g}WHrJ)ROU$!2rC-W%sKfP`pr_J>Kh7oW6Hfu_XOedxe{ph2ZBuL_aT0mU9G^^2(8D?@&I4Ze#X`*Yv(j_qf+)GuoX`M&O zI>=t*F7(^iBs=cQ@$L1D?fRA>zF$@(2LwB*iw5g8f@fF%C$YxTC8G2>TBz+7$;96C zKpN$&d2PXzGx8-R22@}p&Zb5jLUfxSFODi#k|cuial$WFt=Chb6C|#e$143|ug@u$ zX+z&$*%tr2|62JLA)Qx-E?yPT`p^~}`sXOb*M?ZaS_B0jR3rSvA!JUccv-TP~$f@l(Xo1AA( zf}Iw+QQMomlSEF*LF-8ljnKgP|SNWbFmxq7D+TcHyqr$@)WqLDmJs+xw( z_?IgfI5*v@@B7}FKSwjv)u?SjsL_c*vm3FeNs=--Dva)ulOKxAKQVxlRbmUsMw@nM z`ad#Z&#g_9KT}|2p-C%Zw{BKk921A&CneX3?EODTK}MUnXs=xG;IEsGF`K{(+N-Vk z5yNn=$<*^2h$d)HL!@3Vc5-I_J86G^WZSJHDMU?O_Lh0^GWi|ZuK`TuzVPVOh- znyGp1?}RALDcBsXU->XVlNS5obuud0l73}X#>*)MkHWt<0|%|qbs+Rje7}*JD|BlR z;n;-Oj?Wnu&wqm?^&~g8WW{Fz<7DPZ(x0;F@<+Q59b&cM3~obKq<}UeFN;s>QL8k6 zpkULir#;!>3&+Pvk&tJk_!e$4^*l&)YtlWveG_)?xd7mdPwM2teh#$xPmy~R@c?<8 z0&nC0zEO9&E<)Jz6}qu7aI+rO1q70(iTNg#+&Q<`kf6FHNSkeK!P$ivY5LfKLWt+jLTpuu_vnLi$5f(U?=Cff8RSyW zZ=B5*@HgUvCd#%9s;>CmLFP}qe&O0L?*!d6^t??!BBL+FNil6o$GdP-o{V0zbHF(7 zeC+wD3ALq>#DoD}iH(7cp0lfFZ&R#Z8?=^AvP{2UtUUVQD;Y9p)o$RKTrx6?NdJzW zU+nNEqaE|fZWl-cCqlwZ7o;EYKeb$Ui2fEY(9aQl?}1G|=RJb0!GO@(DdKa{0Zz+y zGp`Z~#2xGcJ6+%eJ;blEkhSZdO?Y5W=UE3TPuL~g<}r0fBU&F)M)*lWexMC}Cmrc* zT#*ii+(v&w~z;2>RGq~|$08a;>NKRZz1SLv(S}O`{-#Cxh z-%Bv7yHubj!)^OHjeG19_fOZMM#y7b-1Hm~`+fAGR+j%?a zc-ZUBA`zhVy~o?I@}5DsV#y78#HwT~_k1Q>gR<-x^x zdHi@KNp7XNU|DdGrvp;k5wEB7`U1LKYpI5IufJpEASJzi5tc|h9mobH;YBkBtVd2-H9pz-!Vt-~LA&B!MDe*b%u zG4JdfAYRO0WH+Jh+&+u=29}3`pI_Et{}OmEUu;_&hXqmsz#h$A>m@aaVuhP^ybs70 zxe(UD6?s|Qyww6(IMOxv)FRh23bOVUy*M28*`4feY-4S^avGoNqnyFykN#byj9r%c zb<@fgBR|BLSX-n5d!lN{YGiL{DRH`{Se3AiPx^e`+QB^3@Y@sH)CMcvI8%ey`z|kQ zWVUZPZD~fEJ$@omu^~Hr;Vw($p^+FFxclgJSR>gIJF+!wBbx08VD!75@tq*6+D8a}=Ud&g*W5eOh0waOuB)M1DVhkh{Fw1-1PS>%;kPh2*N< zZ`HfwPA>Xj{ydjUj)tkSlg7Yi?&j6;qeK(QY<~063k2P$lX+rVn)4iop~J5k0h#i3k*nKE9;b~2(i{LauCMt6JycUu`z|`Fg!4bUC%emG%*W86FK8d?6h_$< zPIK+^$3U}UquGEdigl`D8J{olqlZ0?yS*DpDaqJRn|5*J+x{z^3G`zgvzd&Whz~1f zG*Zh+YxNSuoedmV^~{?aWoxJqGJwFJYP)*$ z2PrWhMXcB(N%VAI>Z@z|U5HWdmYmmSEx2UTD~@$5Kk%3I!4c&hFm*Q9YyQE?pVK+j z5NS9wwX*6<#m9x;lOj83F=h9riY7=~IuVf+rJWn0CG;Nx`S za~gqx!3pVQar_|^qJwyuTNTudAFcak=yDR;_@}Ys{j85i?`Zy%>8$^@av1na^`mVH zqkEXLbYy$X2s0$!L}KQf@$6 zNsJMLlxpVje5<4040xAA@boauu&>sBQ2e|L?$rGUPB8qRP+7jA_JFL(zJUzH{7hTP zN-D7x2ue+z{(zVo%`pzOi39TnB}D~~hx{@<9@Adk=MI~4Ye;01(aUFmowZ7=)!&@8 zlgi9)355Bu9c6%VO!i@rv=Yk&K}lJsHK?Jrg_+$`Q_T4GEzq>SA$K-6%<*w})pti)k)^UZX`&)+L0$y@pD$m`e zY0_&E>r>lvsB(S?KiDHEc9jWZGPj()8g(vnO0HIcaq(xqzo809>yj^7$1qoImi*&| zFno7!Ta#jDTJpRuvokl~Ed}7cTd(jQKoxF>--n$uGD)E;CiTmql9`?3 z<7%?XuWw{#eF!lRrQ{FE2IA{QqUwVbIHvnp_>^M7$Xmu7RgREkI}a`v zfHmBIp~%?(agbbW8gS zI!bVcZD~SJm|uUyS2_ga$R_R8%I6?yqf+Ctx5WHV1^AxYItdOGy@P+D=znT;d}eWwXBCgg zhOs3}%Wx|hE6?oi?jVrhFO^cJAEyzqA%AX`X9>>owrP0jMHK zE-C~48&4dc&b{2R(dvOq+5#u?oKcB$=R_k-&IRCBTxpPNSmV8(;qkJGg5=7g% zt=snK#fv|mbU_xWB?exccKKtz&Jk}qj6KsH=z4FA173A`4ICy3qAz9_FL@L4I0=e5 zXJWI2Iqv=U&zNcwKYx?-?3!rEi;fS7n*Qo}VgJ4$B&6T>Zd_FUgMIz_Sk5m7GI6($ zhEc4~`<~(g?RM30oDj5)pqZ+F6i?e_H`S|O{cBS%`!XfEE6*MmfG2$gIPAhf^hW23 zW?(MiPH+VHji#z7s}rkh=<6<;vhj^Lnr(+?5l`s!X^qyO%WvA+1Z^YwUP)!^d`ySj zHd3(>zNjgvAWWDQ)?PWxNjA)(Go-?+Ei6Ao6obkxayzr`< zn-ci!c&TC^JYl3k%O6XF+6Y-Nm$Bm$zU1hxSR0-CX*BcC&-Sb0OTRwbz_#Eao(Fch zKbi<*QQ$R%hyweAwE`?2Af9SOuN8>BjqDu;?*NU;fk0iaJ+FVHBCm-f^yqtSoY8sP zRKaRnK=%FJ3$efx!A2KoH+U0B^&%@xCJ3yv;qY^q?4jEj36M zK^%nk#Q*EPT;$>L!ee5N>uk?ulkanfCTnD!!3iarr%OmLddudd%8iHNH{XDLfnGd+ z#tWFm-gjT4jUs_D{!ek*KYh>f{lx3uA3=&LtX~p^*qY8LjtZiTU)R@WA#S6kEs8pv z9oabc;?EJW^?y2cWCoz8q{%w%2r`3mKzv;_@L>XO|Gi6Cm+Y>iU*`$QyPkpCj>TS8 z3TtO(-CU4K1iduc$t2$SoN#8cv#r#zI7k0^nnYo?#2!SeJ)bHsrD@9j{myrJhtF@0cyI0OkQ?kG^i~Rt z=5f2sS1Dn)2ef4KirfWYK1UBVazw?)skCAv)HD26s5lvQou`M_*2%;y3(p(C%-{D7 zs=jM-_2h};@1;RgGMW$8%u2R&6P2UcbqWEa5 ziqB=pl(5f|r-^zt&NFTkv>5-TZB)du>PI+cb=d4Cv>S41R9HV2y7eDEy#}&a2szp+ z)DWh1OFHB6?(WWfyxyG5?=c5XkM15lJ#Pb?jsohg3|p&zmzpx)uS@SXBTv#9g#nH5 z%H8@WgR7;H5*lmrL`Iu^(hs*qeTBwjLWp{=25=OD>i8>~)aQ#55lkT}b+^&Z{Gr{5 zr(>?-yz$QDt?hiMnAM$wHj%0Cm&V!e*8ZG5oZ*N+{a3SVE1zuu`T`=d{G4VWY0wqQ zNgwH%PYE=Km*o?K8iS7@RiT)U%LQ@JxisDgdSHznjz-lj8aBm-oHg%|7M9|Gwfr_x zAvwf^wqU~3#f^R{19jkq#|?Ta^5Kp7+^60H#X2!tFVD=YO zo~A1T@ZV&Oy;!~&s^c7ibgC7WTmYw~F!$C&%k0>bK}YzQz*~i*XGOLl>`) z=ciBdt8z@$bLY8s*-2n|E#mU=xkbz8f9{0YU+MZUx_uCzH0n0k!b*SG9v;SRRqvpf zA1t^Z+j`kU{!_fN=Wn`a+vMoafinPS_5=HUoRTsJk`J-TT5;(}Yo~#Jirrx0yH$@* zlN1h0cz6s;TbC2!KDhX12i8Nc(Qfy)P|e@xM>4MrFdu!xptx8zWm;3|9qFHD90n+ zRp-i>;KkAU+k{=~X@R5jR07;OpB~~hq1)+%q-D7P35D#>qvo!YnRIlEUSR_WvT1<7 zUO*A;S$mbZiyHOvUVXSNhi*=Uj=Sv5*do*7-*Nf1xCf8KeF$pYKp&ocXzW-(X$_q) z2A`^fII@WlLr5(QHcMscXx$Z=pAfL0@ayFBmm61gH-;ku1_Fp<|5S@fS{OVA*m0{x zTE6w~CuZeBfGusXlRnDpQlsr%TZrkJ!^3k1>z z4C)Gcz4v1nYwPFy3pITljMEyFTlws}w(;dR+2YTXW$W4B4tf+s>bAX}Yk~{Z^_`XL z(V?K&E^A*ImCqSe`gRz(Hi}0W`-Bha5M$NTv+)8_egd^U_T)(<3Jxyy5wt-}4c2kK zqNHRWcmJJ%gHw16bhM&}VKZn8RpzegJ!GBnIOqm#30XD{w8ez}ftER;+K)`!J(>hL zw*&^=+t-QfI6pxt)ifdf+PW5wV%&tQkQ$y~rI4uDoq)5KN;N2p=&>)=O|7y%KcHXW zg$O6D^0sn4>OI~0mQrH}nZvAqEGm{+etx0GOn>6)FCYs~{l2u6-)F_?bjq_6R90$4 zbDq773)nQiCSq!X&Z@=UvKKwwf=g`5Wy^2Imwr`!=buH&994W<0eLljD^CefN@Q852pGqI%p^Q9xoLsqhlgnvwA$WW%sF zMJw5yZq;)>XVJ~q=M-1&UZf)uVV{Bvw7nb0h5T0$@~Lm@?OM|cu6*&ZnC7U~2y(5t zuh(+Bu6$FVN1NcU1IrEn?SQ2WIm92!^XWYm_CS)pdm#n9UbZ=lbB35ANedWu;1>_z z3%-i-sHLM;eCvyR>pM@MWQvAHN`H(#dvKDmk<>?7|7G+QrP%tsG^drFC{E}8$fMlO zap!T*B1=o-M*|o~jIpW_bHeFaS`E;}dvl_W5QOQ1f3YL;v2F zjqBYap646Cv-7NiOOu``At-4^i}4>TWhPDfnu~h6Ujw=1F?_uyV^iN)FQ*FJ1f!Bp zT2&OD3Lr;^fAk-2C!S&taj3}@-!>Sm<*d_e3{Mj(kJ0EwoILlt6*6>E18wQ@(J?*oIxiO65*2D$ zT|HZY;D=ZIQ3dGOXv7;B=DHI-%(CJIIwb}-5^E?GWBz-PB<)3YJ$?D=9DYc3d<7oh z*!oXeEpPg|1x4xQ`uu?P=I7&BS()p3P^)TXltQRp1$=u75`1-kdwG7bEWT)7?4dc+7aZYW_)a9*O8 zI*pEep6m7O_G^L5K_qbrc#<9S?v03lKp&MqCOv^et3FqQ6JnXGmK+pxN2~HBLG~_C zSB)WnUJ9m7d!--Agouy~OMs}L;*mrDOJY`I8wDJn+~hZqiv97RU=sK<2eqAol5IqK z&Cy~X{o7o_MD5kH*&jXfw0-f>=k1GMf5sm9)^qlaCttMhzVa#r){g!W1!(vf{Py}- z#i+?5vIg#u(OOvW^?_k_)TNWSGC*+Cj?dUlXX6e6CJkAd;y}^Jemw7DgVWR|fL4Xt^|yv6U4eFuephZM6!PDsKOyQa zHS#EI4f7G(Z0^YKtLopm6YU7`fevk#n9>QA@`Vj@)z&1p)@#9=LD!SV=*g7S$$l(& zQUYY-c2oXD5HWxmgDoArF8(13W{OTzpySMdQa5F`v9NG4WeGZc(L9c}@a9#W>bUgS ztz2$*^8wA99J?L$H>#k&RUCHLI==1tU0e7l!DJ(kPd4V-J&fHU>>uc`pM#e$p$;aJGO=!5XP zdgEk3_F?Z{b{`zR0i5od%MgBN4aUhz$I6bMYIkPVa1US+UnF?Qf&FWAlm5FzSFa<| z3;yHJe8;}$k34OE@z`_r=#$Uelh3?lHco-V7>xr6xNEn6jNpduXk?HUe=D}* zxKD$}0!F?(Zw$+K4NSNj96m3Qg|EMW@IV6Z9MEJk77YTQX~&WS>vlb=e=<)}V2GoK zFSf(C+mE2%`E}rX271hSa?ac5J%Krc*nOT8#Jh>V!3NLkE)9GHqucRv{ATb&-4sTg z2|N0OejmaZbRI?j7k1YCYk(&5;jG;x+ZT@Eo(+$iL&b4dhC~?8I6yN07EKLA+O zzq6|*f>A8#h~Wu!N5AaWH}SUI!zKBz_vvyxUKF@~O=;=#5ux|eu@kue4Av%Jn6`zj z%)ixtWB=b3__--$(GJ`NM2+xBdqG;dYC^Tyn(_D>76e58(UthezHSX`QydiCEu??R zd)NO~=os6sAM_GH2d*Ifx)!Cqb7TAK#_tYwxVr^}n;t*xg@IdH$k_3S9is$XHZ)+RsGuvO4yz3g4hOE$Eo0DE+0n34_`v+%`rBQf0Lu=p+okb- zxKgUN$Hl6}Ip6-(?fy7C?6(Mx2xbXOlEOr2$e3CAq3txh?ndD1bnE)TW_Q-OvE*(0 zB^c_Xi*YKfT^~5#23)kUk@I@;`t289u|N6Rv-YKLK5t)p{CWG@6VKZ>o_fJvc=;8} z1SX&F50Sw){=A)R9FE=M6WMCe4E@~x#ebhpx8qI-j8l{Pla5#}aIDUDI{AW!S@=Ip zG>u~$`*fiHGz{Y(>8^kAzybOyg_*tWtw*8%fp_0y55N2E_M!K@-9GuD`|RGk?vTPg zr*j6>qF>wb*aSfOtFY8jCmb%sz>kdltucdROcIl3T8r9lallC~g}uh2<|w?m+{C9C|ZL zzNi`h6I1{IAOJ~3K~x-w#1F>CF>)RwKFMmXUpJV`c;42dK_7Mgx&n*`a)MXLrAUeP z#9NP&08$`u`iCrE3(1A>G^z>|lvNTBq& z{0Y`1p7ivCUKrq3t{-!BJN@MU38UKIh-VT{8a`m&LOG1dfSb6I>CJ2}n#bZouo?f> z{Hx1w&jE|KK?-zIRNM- z2N1j_XSU-HVB-DB9KktCf4y!kzY-EA#QPv)+>U5&#Sh7v1{U>3X0?6O&;6ho=sE?5 zbc_*rcKkwzDbcOJLm>Z!$OQ>)vsASZCi!Eq2v8Y32)SAj1OsrP#``CKo_ ziMFM`ImRD|rp(94xBL4-ge@A$J)`7@z*nU>Srquhlk+-XFSa|Mj|GN#&M}Uj7xKD$ z?8SeWz+5mEd_Ny%ap!~MaXtf`bFOPAlKFwd%!1!(k^qpgvfBUYbh3Bc{T6%o{dd`e z_rJ{^y#Hm}{s@CS)Z`^M5Ed!4J6TEM?+F65zBXqMu7twe<#rGu~^!%pd--tu`a0!>nGx=>2NJ1oFNC3-4 z-0>g1Zv+=gEH&WnT)j1Y>X_JtywD%~Y#TVZpE$dn&o_>R!J9Qvx0r=v0-6*Q9Y##f zq=|VKVlzI>+JgcHqvTl|NZh$#FbcFf`(r1EZ8&BtfJEnVz`=l`Jld!dC2&ojaKh6} zgQmhUvvR!~LpzGW{pt+UR)7KPH?6XQN2X*L`Ui1hR!Cn-)FiPZHVdz&4w32E?RUYn z9&J0sgQ6Nl{}pG9wxS8_<}cGf2v*YH=s(I|;uMZix_#U$-+#@%{?yC%wI^S)uRZa+ zee>Hd+f&bd*PeR*WqbO$@7QxMe%H+4dH3_Q(=etlP-WjcXMVOX@kk%PE2Or^80J;_ zF%91Oe^m3w9Sr#?#N7Qxej7aJzPPgB4Lcoo9!v|nJRVUv-qe4*IFNZwVz;!{!)6x! z{EMq-S_sl+Wq(3X%F0cr@iMLciIQveRmZ3 zcies|>W26dYawRfbyn3HBX+gfK0d&28r(Z!+J!{@cUy0BvV>Xh7RXpm?ipf6NO4-# z)lvIv3{S>3WK#RB=zn#&$OkG)w4`ZvYn=Ja3g*Cvs0lw|hk5Uhtgig> z?PEEn^r2)V>NWtee{BkV2@1}QT@jxgV_xejad6eWYV_O$m+(2W z(&?PXJ|$ovA&uA8q^s@IfBi|A%&qF#I^5r#<%V@7OoL{jxp&%**z~v)?)Xf956o z_6y&$=U@7sJ@?{w?fb7Cx4`=w?_-`h<86SkAaGuJft zW#9ja&-_jIZD-)iaccniE`N-{%xw5)0M~GT&qDaE)GY%Vuk9A!arFAh`8DgG~&sOT>7sQSMG=Y}z)RX)5X=cF|kl(uM^#;fQ93q{t8gxb~#(;t6w z93gOd5jo%@giQkIIP*Xp4sSG)VK!cDhT(d4~LhIO_Ph2G*T^CZ^nd0|)a_())OzuMR}sR`MHw#gTQ|wc}$A9!GCZ&JOx#(3?&OVW15qUMYn>VN;ve z#epsQoAK>=uQn14Sl4M`1Z0+=YeLs84ltUWjQxMiz3a9u%Wa&AJQi=JZE3i>-~R!- zhugAAiKNJr{bLpoUm%gS)}~0ei`r{ePo{yBd4FMpIOaJ9fAh=u?XQ0s|K+#8h=2O+uj8No)34(H_{ZPGKmGO>@n8S*ujBvv z$KS;N`H#Pj-~Rd+P407r>frg}j>S7o|A{v`pD=bK)-mq=>f9Ucz6^zj@8RDqlHCpi zZ(N?|etM3FyOXEb?x4*$Z8IJ`4DH72s{kGB)Oq1%N8(@K)W7Af2X40fYdQ?!7{{T3 z8lFbH{T_Yp8~1E?WD`-|)|0 zV0gc1_xbGlqunhhLtnP}_vCR%hgv!8g6C=DKlSTx_Qg@pkQ@X1kls#0MhV#Swy}>X zn2#K*kL`BrcImv4_*X9YGF`O$e2!@2!q&w-rQ2%b_BN;XCfK1^R>O`z|NQeF!3{yW zF3+9V`xxK3ezg50+y5WeL=G(fs$ayjU^R{dWCY1o2LTOUtxZ(@x{pOyxq0is$fvt? zlIi6)hVE-d(TV+qF^zEQj}h*nzK>NZv7rZ*1=xZRr3!2}Y%;zgm%LYdf9`L=!^&k_ z8*&GHufcbs!{nrY~dm}(QxdkT|M_?v<%*!`_%$k$%V?f`LB3- zV(`$P7`&m)1^-OXOR|1@i(}6{iTGc#{NYc3jz9g|U*gYy`8j_7hd-4s ztrhXRfBDm+pZ-S!|M+iziNE~iFCY6pW&iLWeu`iG!%z0#%U^57fBe-y)Mxph=U>Dx zfAMMit6%;@{Ng{X`0cNM6+f+r|NPru###}-{q-;6H^2Nv{KsGaL;UC8{38DGx4(-2 z^dJ8r{>yKE@jU-BV)VEUK0^RR3pL;$>$3l?uJ56`E8q5GdOoA?(7((U2-zV_{q4B@s^Lpjlbs|pZ4}bCS?9`Su6*kJux-gM;o}FrFXTv zC4jVzO%5SqYQG*RVQ#S9CXH*RLu8q}t-txAIE3tazR8xjDN=vqZ+52rIKXQH;O*@I zon@tS!^b$=J=9|Ocl#0msz8*UZ_GKI*`xD<=}p16$_4}2!SB=V4gZ*zO)nep_}8X? zcHi4wV<@`qX9u7MeySK7&pPA*z-_}H?S6W`wGj~l6oRI4Jf0Lk*L(<*QVrz%DRjjC6h=gAXJHuti(4O|j|Eqp7=8fSKpYbf zI@p}pE)nT()dGG;@w?rcKyhn>IKPOx)W@@>`biMJ!}=Z?l1Ufw&5JGRm6#ZKJWeQ z%vAQ(TYrmV%b4Q2IF^LKt|Bh%x}| zbb6uiPj6fN7g7vd!TVd;NB{G_a|ijq_c{A@{`n5jzWvJ1#QEEsY)f8~Q%Zk7Ea>=0 z+n!t#Rd#Mr*5FreqgT+GKxinpL)}ZC@gSM8WL;;DDK7bR9`oOmYm#3Z{|Jz-4m9R! z*$(rJnAye>-<+KCo%^r7Q1>zQZ#MCKtQ-ci8~mG22k-Qxv6R+W{${$NR2#o|WDD8k zKj>>?N68?2%p3pPCZ6}!mRF0->!N(Ms3C0jg-YFUpX05$?=`oBOw4~b{=RW;&+W)r z=E2|$i_t@=LkX2l1aR26#ZKd)tT?8!9LMp`ri|u29V?#+$jwv!J4F3_-vvJ$AoM>1 zXvszcP3w;OPrn|MUvJiLZM%Q}J+C8{njY7FFZ$P_JqtJ`C+}l5pu>1aeMC6Gc0%dM z5knsYackqxD(^sf>Lk%8*EAX)@O)5TcC3aP3V1KQj;s$-R3GU7HaWFGGJe{Y;oZ@C z_Uzl+nrY;;J+CTfg2n`U9_Vb>+cqMx2`5q-hOzcj-_o~)!bZou#E7L^IqI2uY5fl} z&DEI_77_u=0_D@L~UoRVz=hgI%x9hyCtix{o^8|GG@tNxudhHDC12f%Zj%b4TA zvPp_$4dVm-%5Nfse{Q>;bVS*@#yBEtzP|P5{|89y$|X1Gct&VWa*%|Mdgr>8lIj z!Ko}avi9Oela><}CtdY_b;`Ow@$bAp4i&1LI|%B<2MV)kYTUmfmLVS3R5Td!ubyhEC)sPJ+f}BeNqm zXNhRr%EJNNFIwy?X*ms0bLG4n{vTdm zW=vmRXB>5SPi!W9^`+`dm}5gi*B`P)|81M`y0VUO#PSOCX7m31{M(NMTboQ4A$#fH z6X8hzdT_O#8z*Iimfq06?4Y&>adi55t3TXk3BzDAO9sFrpK{?w_D$ZP49Qi+OQ8b- zVs>ES@XMF~!L5dU z#wEbF14A2*61xxmi$lnxG0TK!VWoQ>8`TmgH_cWPkGQ2xoRsSE zCngVlQ`=8O?aP3kFg8$9j0HJrwBuyS!zmnDr+q9Wo$M z%Y;<6U1e({Iy&3HON_Z#sjHRzc7FOt-c|yi`s0^va>%XA%Ti!{;h?^?;o8vW!N15~2v7(4WhX7ckz}m?yR5DK zh7JeVry;(3QRf+qr*f=<>G6yQOrpCv7oX!Z8RIp$q}jpiw%LL95oJp5T9=xO(U^T& z^jcZ|1K)=>>q_!^F4l85@dnpJoJ5k49R60m#ewAOGHoyC<`_<2_-9m;NDsC_+SFZ}lk{C|ikF=+Ym*k`}qXD-P~Nf#cJfO5R9La_Azh1*mELIL|X#0baRtkx&CHtdGFagG98t zuj6x#Os*(Zo;K)Xn{YbK!e9b%-IOun_;U`=Pf>Gu1=*>3X z4sD@Sjj`J&NyB&q_Rfc~vHl>7g*o*`e6Ry?WCDOdyB!xjm$f;{t5_TKHT55!vn&TE zIa$1*PWqp7>O>j-{*2@OR3_x#RU~_eo3uFU8oez|owyO|dn`l4b@tz1ltr9#a!IEh z)6u54>-UhIV{y|y>P9?8+||Yt;}QS(cC+H|QdV;g^|xzm;|}W!bR)2#ope*($s5O# znC!+i7}I&iH;9~5E-gkoZFtdo%lBe01r00t7W3r4?iY%<#W$nCRe5% zv>EQs0NHKFG&xGQ)vaB_ztk+jq3oi=e=)i)}izkTD9SjRq-p2w!X;|?(N zQ*91)a?CXWia4Bfa@t;gv~i#L2K^uH=>8TXtuMQnS1=KsXn~jAc*G~ ze)!=!Xr7z@Yf8tz&K>;#;dKArUKCjkBixu%{Z!ui7|(9y5_O+)n$13qkNuzVswVdM z53}~$OBa8Ru+Aw+ElbN~M=pYcQkf$FV1R7d`efO-{@(pK5XkwE+ExE9rni|E9GEe1 z#tDmu10V+J01|LNSZDul0N<`lTt9U$8{ty=$2a=7!-Vg?vHx3MH(F=lfkW+xg}O{+H~YfwBufE6oSG{q_Qt{X2G_x zKm1?7t*Osb=aVQ};;l+HoOD#^pHA8JwdWe@#J`M@+gpCw>#{*PVsPHz|Io(&ut78j zfVt?~7f$ZuOq_Wfc$j{~zegMP-(mLSb8jzSXt8zp$(ya##Q>AdPOx!5kUgXHy#JO2 z6ruI!b8r7PM{hSM<7FfFX8V}?40ia`4ER^|YM;X%UkH>){@mMp-_-x|hVpi;Fj-%( z8-T^hE1egn`asEX5McJqMxS-M(;vBIctbrn-I>F^s6Xa2z)-UXacvH%l>gfQ*1px> zxMfTq8aI4s;N<@!0c>{HWX&Eq2lXo3Yi!fk+Iu(9bxexS%fwaQ)?SBrPbD|yNgI0_H3ZRdK!EaNzL}?sU z-7ywTyjuS^Q9{oh-6s*>E~;@Z=hXqgtuGPX93ZirZGR$2-G13&s_RJ#%W}RmkvWag z!Sc4r|Iy8VUxYqS>C~Uu?qZv@T(>UTr*6fnjqEnt?7dCE-G0{th--a;Nvr?>AOJ~3 zK~yax6xp?Ph|hclyhsYuX*SxaD`jBG(`UCj#L9XiqsL>!KICMZbYZE_01gqvmYqLs zlh%*-T&32oNPNa!(=(*V^5D8FfDYd%Ypn+uC$QUDTYo?L%K{s=HQAnDCHPB(@|4$a ziof(P+wC198~O^`+EungyWPeMJyRB6#`UN`{gZ@Xp&Z++y6ktSl}mrsr>SzvvU7ld zb7NEBpJXONv*{T-J!ZDZB#pWr-;(}MvcOoENunD7u7`5(lS`$T^v2zCT*4^K&|55x zupmP&u6_@s?QI^vv_PeVg>_Gikdz+wbDV9fzqFQ?P%j_9bPV8ZtZ=mBvww?Xv5VVk zb@)iI(a;Y!i(+Q{s*fkMQ3)#XaldfA*k}74 zi3O6ep0ljs-L~Il-rnC1Ca(3vPU(-uA{IVnX4^*w%3@si%#r27N61ryy?h0*z5SUV z1I{5=mhstSsodZ*`Y!uGkA3ogOMoCLuonT${3-cSeECR~-231k?Uy-(BBkjcw@vI5 zKlPNkAF%rA|B(x_Vlj@kTRL?0JC9iZg)ydf&u?+glmAv+t}y+S+v|BX1MRKWw1MzX z{`gqktku^q`J_P*^j|en31jYoFB9lzX@I>eovXvo@~6<2AZ5rfuHR+rQu%bs2lp=V z6~YSt6qooN;uc>4T35!pOR_0hsCA+xSB{4h>&3c65(MkUTm>#lQpy5bzgS)XzslU8ep$GbU&2e4 z=AnP4O=!-o0px94#N(mq-Vmkpwo=od^AUkdHYLuLyk^`O8I9W4SXNpI55~;R!c$*5 zBRem5W7~ca9|JB6^>X=EmDzi)G|+)>il2560Q~QIX1f7I6+_r%jrh;x4>FB)unYO6 z=19G_V#w@f{T2Tk&sj(GE43bHn*4YCrLeZ%&|i~+YlUIA5m^1BUAz=iUFzMv^dSO=6ep|M6B!6#LRW4#54u&IC>dFuA7OfG(5c!hPx>LGCAc z^VwxdAoV(OjKAsRn7%$|^4sTbI2W#e@?FlyO*U(#an{q?~v$?n9tE&F-iVU%r9J>c+IZUSs5GLx}}b`!DoI}a;EIZz_F-EF8R=oZH`#Ym;v6= zeO6%Y7Uf0!!KgD%t#M==AjSjYGiZ=-=&#`yC0Elg)-QJnsUUfW9KCZa@NiWm$$DfxG$a==Br0rtsP74ud*%#42goW_vTuSO^ zpgn`|H^8|ta<|o7(ez(0?Y+K_@v1-UeA;UIT4z!sd=b7XzqR4|#%#XxF!AyYnN`G}P{2SWzvR=-;*GYpX1G>$; z-GxSnv8L#~<6qj~%tbVY4o_w4#J_&1!S%1U8;6ihKflMD(1w3g*=_mjdf|+_J*MYd zXwKXc-JYBp`LDQR>f<=sIi~WxKBnp4oTh^UlKqZ<>o$hz8qF^zKJml%vPG?{`vu32 zR(@T|Oa5EB=0s?JB#dk`jsdM61rpyKdcYm#>_j#HI_&5Jo6*KzhUcx=p^+_S!fkzp zi>d4+wR17hm-$$4Tk##t!9IW4hZ|S6o5Q|;Jwa=3D=8mYwrB+WP5_NVnw_eh#@az& zHf5^Lra1ao7nM`ob+-93{A(D0fjDuINA?{7&+?N`%42x|AVv29mISn^ttT+|4f1z4 z+26UowGoewGpC$i<-JVEdLQcVk+rIi&*f!x!83Xyn05XMtcd8?4UYtIA_q6A>_5AZ zNWyR=fsO%*0Va}h3~y|*nxrr*6W7u9t4+iLL)wr1xam0CxV;?U*QI>MW!l=-zfh(V zFkxY~^HBz4*I0Soh-7xz^WK_*6!2VcWS`Q!Ki1#KLzB_-T-^3K9+NLi+`{Jc#1t!b z-3>tLLilYqDBkFaTHDkg@dI0Ps!jwny@}H7M|<_JPI|i|S$*3LmC;849An1Uf6%`r zxD)<)jId8zfB3p%agH;6wi#A8EJ8~=9) zm^b=;!VcJ-KLWOF>km6q+#K^YesaUo?Aot$4?AphtwXn^%I3rL`(^x*@=v@0=a{}a zJ@juFsm(k%V9LY3rT+zM_a%)#Hn}Q4akjruVBHyt=AF%zPp+N@d^zZ-!Ek>Jx(EH1 z>hd1r({4BXE8ZZ^!bbGBfZe9e{%-enB@O}q-tynFb+=<*jG>>~EllT+X|NXw@2h-F z_R8{)saCU}+o&G|%r^UUc=!L7t5~dU==_dB;gLqTLZT&Y^ZgwtDSTr{`GLs5tyRWXP?T;Q1{H+cYzB<@A z6Is2TxsRFaC6}_?v1c^>x@0J?3CP=cKDZ|rQ^FA-ANm4jJoWc>h4W8`PTFTM$-8a| z8ulYjB4+O!9j+RWL^6S6-A4yWClJojH~b}biP=DFd3`inSzT4#-{{Y8ASNg<+f2o-ZPF|W4J@=z&oEYl(=E2vX zxoJ56xcjAiM_<_8xpkTA9?RdvVNRpIzOi>Zy6xT^;P$nctV+Fh*=nM*yG`2xX++`Z z#0{Nea_sx&_`)&Wmfr%X%XRRpV;3zoSzGq{EV{LC4kK}VZN5Al_zwPc$DZ1m-i>Q6 zdcBsx$xm*XUrtQpxUOMwKvgy#%0~lrfh8|a$AqWzg-_7^I@I<^09oGJhuals7tV@_ ztuy{re8kDmU%LEOEZWtK|CYnmeGGMNG2e2yW4-hF3;gT(Z;zeMIVLXG|KJa`^!VCm z(-L$awtIx*59)`wn(S)$=dQS(!s4gxwmZV#M7+f+?`kPN<{S!xPn60Ky(!{0SrJJr5 z=_C3#!M-kG5yucUQy<(jaqK&;bC(ADw6^7=!aviWJd+YV7@Gf1{h!D}0$%IalkBt) zF&gr$T1odYS#KZ4C0-l}+!ROENxuue(77htee|b1j~^HQt%_w|j%{*D|9rcjbBdqR zmWq`p0>Leri1^r@JAs{}LDC;X>(lVB=BwC^dd_;d){$H6Rl; z6aci7=)=a{1)OI}2kSkLoyGZaJoFX>x0;nFVJV7ZN+k!d5HyeBQV|Jy-9 z`N|GUNXl1AA?UHg{DM0Ij%^GLU3+qz#IpCrx3%FXw$cqJj?Z|?<(if<;}>*M{!i>G ztbweZ5G_HS?ul&`$iUN$O+3MLX%oXYY91pZuEYOBhX>P}6usGSJ@_`_W07sY{a)i8 zM$X3bFYav z{aig90rVAsc!l`qiRvmJa#-^>R?=pN`(C%(DygM`p1S`I354fe-JQ}8i!;uv`yjU& zJ=S@!v9UJBH7yThoYn^OktKO!eJa_>@GpNWmrEy&W*cr`y0bQ(D4klp@qe1Mr2<;! zUF8R7h&GRDt;v}HI>u#_rS`@vT`eSlWqUenWPP0W>G?0#ywS{0gN|7Qk}Amk?Se?PqC-fjK-^UwR-eBSSQ-OB%(cD>Ei=ezU$$n_na z{ht2$1i`w?o9-oo#-}yyw=Qm8VyJgmANm~?t-TPYhw+F*rStMOAc513O$7zkmYCen zGNCH)dKyTrHlfev(fd{TGe({K=RM#(=~gSQ9PMQXj3C?DQ*Na)C(j@FyVyLV~Cu? zx;2PnqD`M3R+We(#wkzxz+k#$Cv&_2{*iMg2RP~VD<}Ds5&v$0ocLGVQa%I3EZN03 zuBSHr!qry(JeR)V&xej!T8qQ4z~&8fE*ulI(l_%z@(1~X<8~b7O2pnx|BeMRi0kio zc5ulvwsaoaH~e$C86KZ@FAy{8_f`F=|LOi|+gPjo$;z1*VB%_Kom#wuc_ed4%P>Gy}$YTR1+4%C7!Tdt*}LlW&ygoXr2kxe_orMEKXs zsjFLfACsQ#;Q)}W3*PbXv}56#@k-XCyam9g+Q#`GS25fCA3{y>xcdlK0(>>V zG1p{A8sPaK0UXA?T;KBL%Xoi}4SXN>J9Bg69`P>!S$7i6Id%XrTLKG^Qb4hN&Pu|0 z7S1!5bBwy+l13g3=dUiK3F&|I#hV6*Ht(r$5K<)4+GEPn4MZT~3A3@uOCnepbR^O_ z99=-Qz7ffhRHto7Du-x8;s?^u8&ip&_RdK-hAohh?Qq}K?^j>`r(NPH%SlYOk%4WJ zR8F$!?e$l>t?Cu~CtG|4Do+n!6%8=BPIE7{qr6j}FdIMmPw_T! z&o><4pxU~?M-n7Vk@0RSrP)qd;%wVUHP^Fb z9Pn>if6FguVZy%!!P72X|Lgx7z_+q~KRw^d zex&|yas9~Qz?DcMW@32C1$=%=7A@!~M=tdllo2C1?(RGtno)-K=c#G5e%brn$WK^p ziP3^fvRxVW#QxG0HgWYk;f7ECs(vSyd$4hg^OU#&myLw=Fa5cZ?{8jC!B* zkYze-d)D8U*4s*n*>V*h2!Nh&96B?2vS^3L+dA#k%Wp19eZ1_^pZbC#7S`!FNFJkV ziq077Zg2ieGw84JrZU;r-(;5GQ~vc{CYK#}szA(^$*&&*pZC7gfxzvMU^)m;T{MfJ zeb3p}@GVBm|B{=Ij6TAhk7d_gK&wOF&Iy#M8*9@0)c>}&?_=~hh>)%R>I86VZ-L#h z4l&acMQ&&#X zg1~Lki3e>9oufVX!;t_;*39A8biUbOp!D}-EuSTpZ_@kQJ}%pINYH)ay6KOX=YHK> z1^v#A{QbFOHNoFD94)!^jXB!pfIUuD8;@-xwEhJCu6GP|s~KQpz1zmsrZwC7m>WYI zr?uZM-kmrm;)#Lz2GH(_X|J0AwDob)jqn6+S9!*r^j{up-T$foH_G}U!OeE|+yLtK zHpun&KCk>Y0AM8wg*8fg&yDDYt{m{hKOawRg;Fs*^Wx!kx7Gw4v%?erCXZH*qCFST*9`&2zyMAErZ>vRxA+Y&ORqK_J!i60zoxqB1xT;^ z0uD}2;bNQ)KgU&rTIv58kZ=0c);IoTjmA5n8fzQT;?(dj^%|bdAIL{xO7#5qj2GI< z4UqU(J|V@*_C5WnVPY7*bnH1bY>IKRZiYjP=A)h0zd67l;iD7}s$c*U_SpEt0LHua z1ptkaL)MzSZ|vW$AMHEI@wb}mebe8t>{HpuSw!(hFt@i*eB({K+Ge%|_|wt)s4dz= zw_SIlIYBFj82aA!0}ba(Hr7pvR?m$a=GOprGKR^Dqz)B_pdq6fFv4~Ph#XLIdq~1& z12G(54;a9ujhvvyFAZ8dCu2|OAnHjT=kgmd$h^bJ@I*Rhv+jL-GwC{U;Cu213p2Jp z0($I0sf9M4)OA5SVmJRE%8#TAH}=M}h{*59_;1ghB)YAsKl`{BG-rth**`Y0w*Glq)peFuZaeCTG&`?QkS|Te_(q zBJ_S|LKiki>0h?GZSH$KspJ3?v(X4F2#`3L;?vdSIvt?5 zHv^DY$~e)UPx-NGw7_9c_qG0|Koqao*0{l6)*oL0lzx5y(e-}&il)zVf{fqa-s<13 z9&5*Q3?WR+c>=?k7Wfx~JxjOwg$)(gJP3I7?Tg`|GdN}2C6G&|{+_S6P3V?y_|N{- z<%oU`+;gyn``B0gO@cS>c?P`VpD7s-Kt6Uq6gfBdIpA&6e|uYh=(kAdhB__=}1Q&We(762?MO3f8N`T;TzuD_1118`me4j zHon#Guaxt;>1QxhLaPP!O@D8P1%Ci*P6}}$(KR3(S=Bm*!Z})7Ecj8nF6EN5WA(rR zbGu>CDcs3YQXhvNWFOjxqaEcaLw~`cYs%B|sT?pmfZlF4QCSyn8iPygsxpB&RW4Dx zi=1lQ`>;G({iLhaxo1s8;3RPqNrKgQ1$t19gwYUp`YIyP+fJh+fY`>E&|mYNTqU%x zA$mLLHpLX}e1!kdx7XjJKTNN~HLm)j-(`38(fZ2t8_iN z{w(vpbj#l7NaV;ZF|}!2()lQS;1y>-ce1AISf~5xjsN?GciZME|N7g1*{QgN;*cvK zza`1fn5SJD2W5SV&kOUt^3D%lZ(jl2%ACm?#sl}QKR4#g-Kt-d-L%ARn3dyPHtX5$ zC{raDt?+M#7B}w1Exb4PzKq%2AGQhT&-RyiLEEX_IOv;Dv`qjr$NV?f{PU~>fy)b; zXYRg9_issbjo(GJcsaS%CO&n1;~ZD*(O8ZR?N*JqBAsPF zW4Cig1I-;8>5;I>=N6JHea7id!ZBCtah6-@lI$s7cmBMG`sIi@UP_hiO|-JpW1HP1 zL9m{|4MFmrk($DkKD3*JklhmeX&0x8B#3-lveC};fBuv5sC_d+`q#e4ht_AE7z3PG zH2TKB-ZolYGfOT(%3t6Q0Y4%`f3`D%rO)kf@6X-|;QLs9*Z)(fH^Lu5^kx(5@c2@H zqa$M4Qnnmj{mL(&#K*>}_?IoJ-O`YKooq?8{FZc0F0dl3W|jOYvg2@|Y(vPZXUvcd z)2erLsPrN?su@EP{>`^k?r~bRDq27QlGNK zD~U%ton!Cjlc`&7_Jn^;J`>M3FaBmfEs$lqFzVD_(Wvr_k6q)OfKoG?ZzW%R%A@*{ zvvRY&;Q-06`wjmfcHg{kpFqx~drQp34aeK_jT*k_>BOd)VUzBcETilvDCe5(je7Gp z2mFc$$ewvgt^ebjHfFYk9p%5}tObibCo+CYk-9m$!k4fZ0nUB^03ZNKL_t&vYr@#~ zz&qZ$*3`nU2{iD~FlA^`vRR=&3@!x6WVds<$S^Uy2EBWB3L zKX2P{l%8ozgf(LQ1plQ!IvTD4|AhUBlb#f=5Vv+mTDckAr~AV`NyPNyTor^ymr>?&>mF#IOO?$$57jm1JfmpdxHQwHPz{gID2pZ)Uy zrQ#;C?CFzy5>b}%gY~U;sef6h3J|lhq1n#-qS+~Oy;u1g``4FE$m2TK9`W&rl6bmM zUVIXY4aP;rjE`MfJ~nX%a71hSzgGw7ws_W{d~eEso&TR^O@Gy=-F~{w*@?0zoE_Lj z!h0;Q@fnq)d~Y7l?>qA`kKmktU@6eZTw-c%EYEMI&{;1cSs@FWJ-)Ft`W=!ZUGFXM&`YI<5Fsjf=a;eQ& z9!e+~%6fwkY;4PJAdI}9_MphC4je4PDH7b!KaTrKEZ3>$j2{pp?PNy5nCh)(;Gfwg zF)wgX3Vf5oI*}$xbIy7bzGVT^AO0rUPD6Lv%>nvb(4ph_oIYmxEafS`BgUNrWgQO; zb0qSEFQ4Y%eI14r&&Y!|e3pmBf4QTU{NZp^W)i`9JhDVOuN}rb8yF zTOa_egVM}JE_n%YHt~=0$kf2p$u?-$>&-hEKiLnJJ^Hz(M3a&O31IUUO}L1twWdEkWuD+eBJ<-#z<|vyuzQK!h8OG89POfbmBdp(U`k(8c1aRn2F5Wl* z=qCjoD?4~4F%3x(u%#-{| zl4_tXvbiJX)&l4AhHNusK>w`6Jhpdyd~YAJ`3WeiGFM6UDj^Jsn)Wm-5F<*KGF(?b zqd(jh%mzdDcJN(If7!uoQsl9MYm@cO+Y8WgvzGb^yhR*L+(;lfjxbQ*&P#7Lj^&r1 zJ@*s2)TwE8TOu0*Lm+8tZExw3{6&_e{$%3ci2PoVp-nd&la78ThChuES#H(;)GF#AKVdWw>B9II-Z*xvT}J?JobPcr#t*X?s|h|de|hqk-B=JU*h_+911 zY^|%lTu@jvFfFd@I=3}3+xV9iuD{nS1hzk8czXkw{002u#-1C2`Z}OFt@%v#9pa+d zu_Wj_0;_CQ{g;d_rvka+UbQ1H+{$Ey2VV6%P$1-g+w~np?_C=z|q@g zKY!Qh(AGBjWxBN0jpi9qo433#_UpLDvZbTK;?5h{}j5pfvM4^hx(Wa_BaEeWMwWV z{beUx`$uNROyMAmp6goq+LSMK6*k5x#W%*?C9?=(Q(+s&Ft_FFmE*M7EUrnv&1rn8 zFZ)r+kmm!+S6s#;#GlHAvhqoEq!yk30k+Q%9b!O9Zl<1LD;}`V>=*wGhXQ_$9r z2bU*vPWI1$(3Sbqajf^NyN=w~ zIb8Ffhxs1<{qX%+^wNtjJx)i-&8LH`Y#!Dl$D90cm)x2kb1lhQ?=zPUPpjWTj2XVA zPd1zll}mr0-q3o86;Dp*Hm=HqfMNV$o05-^B?~#G${IFl$(Fde4g^BslI0JPW{ehg zm#-jJTP|t`qh2wV+1F1f@6f}(tNNL`V06Mu)naeeiQ4K;~LvlPWRZ0Jdtm=VP)ki5l5X}3s#4Ww)uX>OQWA(2Sn4| z&7*dWyV9z;*y%#s<*E((N_#w59RB5`@)*5*5@*vE<^T|QyZ&WE9T?G?rq_|L!#LJY za~EQk?>G@8{(W@NIN3D!9YacPdzann{J#6`BkQK^qU7V@%fR?HgY;cqj zu02ct&r`j*9PBFl7*!NWkth^HdUM6GXlMJ9q05-Q4BJa*)g>61|4;4m`I(bIxkLBP z9%I+<41dH@pv+Mu+Pr7)v2DLt?mMw@4!)&7_0LH+bQ1>kvXZvppS?lZiqVd})8#Ji zUjmF5{p1_{%+#E4STw^H;1kn*dJmaXEh;`a@?zhqPW&oF~Vx@6M zp#h$`S!WxL;~V<vk;}nk+f_h#Az9CGf*YN9_&O6ChvK#t=m*4VViRrea?LN!h zZPjAu$Wvwm^qi^dtb^`nmX2Eij(W*ij{I!km?_T-*oAtGzT0xR7Z~+8pvg`HRD0Qj z_Qd*|oz(89{$}6bqz|%Du~vV3JKyX6J=gt9gt?9+v0@;*a&&;@<21bI+rRZ0$O&iV zT-rQw5lF_L#n_*fO-I}&4(zow!8mSNAWp6Hroq78?1a_;`W1ekT`~wh`f~P1<`v#B z9S;tjvhST!_jB1yqH{bvBdgXa=0_NRx2Sa$_^sBD{_Gq5PfOkN^pkHVi8vO32wI*T zr_bb9Cp*w)W3;s8YR^(b>zM62;cOD2F7{(0$ONwR2N~50Js`hqvYd94gWz9#;q5170S|MW&g zbII#yuhorNhO|SXW;esoEpsvU$kx?-xlAb|*JeF)8;4}_wYU5bxK&~o?k)SRB(UM1 zW06}&DxUogn;ltHlMOMpxY&i!&)mr+8@7Ob3;!YSA;$14{?*!vi|`h|#y<}`{0$D} zpm*gY7dx1Pq>PS#oZAk+;QyST_Tbka>1&ze@IHOEAJp#T6w2K2uWTAN@$2%NiA;IL zD#tuG5~|<5ErTl*PyFq9T*d?Ig4|jH3DYw7GWP13{!_NOINga`+a)J;vjP9IFR^d8 zJ~dth{_zz=)Oh~v>Xu$LNtEBzGIHL;`dk9s(+-*cdQO^iHNJ_oHSHU<2IPKQ{kHbW zIm*8l^vGOX&N;Db*(ub|@i)cW$G!B2pF{n{33A9>Fx~^lItN?Mn(@?rlZS2nGyY2u za^7R_(QaJ6zWHBv&v>S1YR%*i8)Mc(Iw*XiHULi>=YxfPmVS}`jrf;SBE>JurrQ=a z7=K}z0GAz`)846bLoBRkTh-!K@@MfwKxN}h###sfF?iNoAHEay4xzelY zY6%q7f0q+()OpJ!R>;92fNQB5X_IFH9@JP|;BS#g zx+MK+OVf(}JuJiWCF-&)HIv>YS*MXyYuP>J!7j<_f^~JDGY%J$;L0TNR>xY=&-ktzPb~yAb}X zxPdZC|5FxOVt4a&3xpa7cz3JcGJqv@+9PU>xEE8>uJtZ{vSf~j25OuAR_TBGorFzf zOZv_EqWMX`as~Ur-7~n*@@oAfu%V58tj(O`_1rjPu9HK!KPT4IBMc$&95OiEseUXhh|-@`eRSY% zA_@N@{hwSVT(_4|spAUh{}%iiDe}1LQ+9cjrJovzGnS;66se&jbG3@f)IS2aCj85( ziT{B-ByBUPWqgu08Gs{8R*I4mFUgKyZy9KZ$r7(m@y9@bw3ocgQp>SH`| z)c*#6>N$A4-XC@G@4qHenl|fusN)-f-ZI}fq#~$t9!~v3&1x&{P zSYR#JQ-cjhmp1Z*p`J9N{O!6mM61E?JFU@;)VSnBwNzjArMwQ;!Iw2#)*W!UT=i22xRcbg5Hec{zW zVUvw&!+P4RHpLCw_{Jc%#-@&bh?!0Ic%Kb-uWNYFptVesWlXLV_s)Nbb!$P!y<5zD zgeeYiXCJUt2Lf#`SLiLW|6rGhpK(Z?8Xp}-XiDQ=yq-MJ%gGU|T#C<;d~-PCdGSEt z+-D)_>h8Yhh&3=TDT_A>4Y5YPNl-Ztl2zG#KY7F$@mZ2xK36=m zRZd^EHEv~mLsS|~>Ic@@!?v5lV*^!txaWbgDiaS9>M6{phHv^mW>|%Pm8(4y4Q`K^ z*k!kLww_QQ`P!kgfdqaB1JRj*$)Y@cftE0!lTr=Z0`4rZu)F-K`v1824kk| zoG}0gD9zySk3UtoXa=Q(WKGwoGkYdce*M~2+7iQc zk}mOR<%HOLkJpr>u4Rdxy*0Lk2Y$RkI7phsTTWU@%!ff50P?NPZicKk8$b=ME~ETv z@H0mGFWRuka+qACC0j+A0XidYY^7H;34!71pM@n`@;tuHgXTK0EbvX9@=vQ>IlQ^4 z`8%L=1LU8SXUi4p{S(p)4;h{FB(?q|KMLH$-qB-B>)u8;emg(U*my6TZbIPzCe>; zqmFa^IWWHpFs=(=z|v10G%vmB|3EVi=wT;y?;H7T*f$;2aD#65wrf}cXFK#`0(iJp z%D51bklVSaMQzet+GQhx7j>XW4=blG<^ITkwq+{CKZfI&w0zluGxdiig0_fg8&-r%up;<3)lt&Bb?t-;U| z1Ofj(&9=vT+0q9${g*cS{+}-F!krGLm;v&onPkrRGW0#h)MFa>;U_Ib2uIPza4v#5 zSjEz0^W8!=6i2q5Is!Fp%#F%~(uP=NhiOPRhaKxwSqU7z3c0ZUrXOEqW?eVH<0JfX zQ+r>H7x5ebh%}&Xk-E#-pKlxex330%2N~GChB$Q5|rybHAm2~;%!4<^KKm3$wnGdG(SCYYg0qkGdE`t>J zlrjUj@hB0~lehfDQ@|6=93|5;pVaRge|47zyV~^e_}O74Ux~&K0H=?wmF;qj<$a(N zKigumruqH3$S`qY0Jy4KL;G-_>Tg38Qw!^*c4?*lMz{PU{1m>nCmVeXJ91sHSpixu z%2E0xdJ}4W)$FSqqW$jrr)@rUmV7{7=6W@9+Yy47YH1dRx0Ou7L0m%O$NhWqn*JX@wHS47gvG-^+(cXZ|rJ%w?N0|eerrxbJ+&}t=4Ekht|$H)A3K(iZ+ISl!G|2-5y2@+(7uj^qJz(V<%9-K~CZt zF$|+o2M^e3J;`71fAx2a?@qHG~e7Ljw@5x7>KEzJXAwTPH9>-kyDQ$E<>z}t9?YXY- zca9~SHm(N#Wk7ZK7tt-Xd;GchW4T?G|7Os}p?}IDeUxJu0Ei4N-V*<~@$V*6=BJkr zc5p_R*j}G=nBfT%v`?G3)?#`48WZ+NIhbQA{L!lH{fx2$`hyFAf3E+=gnp1hygBuM z>T7((FR`#W0F3Jf*j=xj3GU(qfS6~(mHO|*g7B-b)WaJzEsVzBRjMF|w?&-s&n!3P z;=NBZLB>)Y6tl_lzWG;QuuqsPY}EcX{J#RgH#;Y2O-g#Zf6qo=g8DU$X>YCu8P0M= zC{K^eVNhQ6E0YYIq(tCe?M(7AEr^rWDPHrjp_iKqUqRz{=#q2__QI0u8h8io;2^;7j))9H|eyUe8~Jy$Ix~ zd9r#P2I#c$Xh-oj0<-jYe{J+zByf-f19{L}rW*W;^+_1ev*g(#25!{2fv`<}h?ujT z$OHrw4qjOP;v}w%5eS^I>=W1I(FK41=2Z+EtZvL~K_y~t0;Eo#TLXU+h;DL%o^KE|pwefe=IH_3QLip4) z!0B^Q`1dK_2mo@yr(Mpgd+m>=&HH@$hMv!^+uo2VG`HO^FHhNWp{Zw%cY-g`T2A9! z?VI~r^IWUC4+n{Q)&t-8%OB*Eg2v<)NheozIk>#zhCJJ)Jam!CAzmH!JZ*?$4S5vj&p#fb8ZQ9<=$IzQT4u{KH~Ig0b~Cn&pzsem z)o&iOyAH-H#QDZa-EP#gQkRFQiffko?5B?gXaoM@W1p{PF61t z3ip6pjaRC#wUZ5wFHtu9sN6DV+tQVbld5y!@_x5_T)WAiZ_^oz^80XnDfK-7R?{uN z3b1&D^VHj8_2i0$gXV9--0-|TY^JUiTnoS`F2 z#4t%Ha0FaW<0_Y{Epuw3u5dl|Qy>)KAf8h^;yr=1Vm$*Xc>)Y%_a+ZNUE1Cr`lV2{ zNhT76$_Xrfk}1I88|~bvq+}uuoq*P6cxg;JX(o88S8!ExBnU_U)Co8-ehmO~)I#LH zY6p{T=$*3V+Q?W&LS6ta$!pXh<7cxzJeE>jx1GnVMgKyKv~tPj8yF{B;);Id8;F3! zu1sQcj!Jr)eSGIv_}Jgn9ftzA2Xs6l;0-3)voXn+iJMzA;_w6f+9`XB-=lo7UnquK zd=@_xXGng%@%Q_h0ORXTb#jes#!YZyHxsysDLHAs8~8;t6+bo(N}CR2AfLl7@G5+VZ_E5p0#i;g#H;j4{l_M_r`PA2nIRrAg&kgpKJ9Q|p}gcp z8^g>98^8F#Kzmug!zqnFP}Z>a%%$;n845(k?jG9rHe9mWz(km@tfXhj%2 zh$BtrTuO5TDnMAMJn>%3(E03ZNKL_t)ybB`YNwl7-pwxug2%aaZ z&oSwo?dk^-Q=1g3Y7Q=G_z{XwEyH=}o;-rEgiTeS_ArQRw({7(D9lUEZonl60146n zpxhsRu6#ysk|Xp&V3!!+Hp9Q>e`aIHKOMWdbLR^hjs16~Mwk{{B z>%Qh#ER!6ydCxlPzZK8fG4o92m z1Qy^H_(HZ%{V~+iBu^|qT|=C)Fok4ReD&12d}afSmgdQOu%zqX5U~E55R%ye*l|c< zmiGFz=OG2{kTF|1!@`Sd(;tV-X#W{==>Wh#BU&+*H{EPQG-m^U^~F;U&PCz9-f#HE z{%Vv+=UENxrsO6Gf7jv!$XbU539S2A)JoPvKUNP1!3tRO43wVg<-OYZ=^qiNcrZjB z?X2UNU*aqH(ynPksh=Q)c`ZWw#>(Z>yc>eKa0_y&E$qM~%n*43MSp9_1cK8@uS~!I z!0<=5%lT~im+B5;k$`3f4K&NwNri0-o6AP{r0bD)e9NznMKGcZZS-SN+g~ynA>zKr z3hpg7T1QLvhWT^Duf#-$#@w_?f39-~W42G8cGQM2?U3_!Z%Ua~yLZm;M3~?(0Zae# zK+@Gldb!JK&$ME2r^6k&*Rs5keNg6yB9B#elPNG(jr;ggPr^*U<^W4J46lK#Ot5$1 zU)ox0%ljOxC1}(?agFU#)&nc0@rADjCccHYp^k5}qi zm_{7+vU=-ZF}I5X$i*L5@Wi*)33;6k8Ow;}e0g}#uL+Gl6^`4r=3Rf2$s*B4XRb!^ z0(~gVYV|hm1|7Dt!U))-e8Y0K5L}Q`(?>r}tGp%0ga2Fa@*})S1{mulfy&Z`{UwAf ztUK|lFp-B#J9}*Zwb9BPvwxeRQxx7Wl%0@`IBfy8u?AaZ^(8}cZY|D!;CR`+$6^#V zrEQ!?xB67w;ekaL9y3^nTDJ!Jy0k!mT^#? zb;P8y(nbeaHq?+WW>GhAc5w^@rKj1~NyQ<;LFWMGYyKrb&Xqjl`%~#t<@!bTvT2@pr`_~sBEkN;>1V)AWnKL}0Q{}I*|Ihs0m7{*x_+HP+?s*s>Nn}1wl|O8 z%`TnPo0E}f(*I4jRzBb{lg0z%Kp-~rIx7bEKmET=KSuNx*=H8`nPt*)@jr@0+2V>i&EtYum#SEB3{Nx!Tz_Eh1suQi1rpd6V7<4E*s-?sbl%w3BQawE?hv5YrzpZ~C8ZZQ&j< zjOz=1D~FV5)OB+3_08GM$y6uIB`f5XSzKpA7(*Lh6R$km zzm_*^lm#bU;3wRqtRzDE4>omZG$-t@QRG}|`(J7jodBwtiKK(;ggn9$F2va8We z{egd`#VPouE;jkGh~iBw*&sutSwft3>>e_Gvk%ZY#D9M|t0XHcvZaQw5piFuS^ zWrcD1I9Kf|w-89a53Fq#MdCuvFz+WH$;-&gN0?z#FTyJH&q2TLb zz)RiV@-pHu5{Ux%Tf2GF@1Xy6|FAh1#;~h)%{}SGqW27D1T`<)K0cN5=}hbJKrrFa&WvR!_DxV}GOK z`p2>9m-SEeeT+s#(`8tAZV-qk_J$=?MqroD^dD~c&trpGw-!4QwU!2TECWH5-B=cJ z+M%yM(T?>@G{6QYK-TmJaN36UMWkv&J@MpF@Af~*<^*^b55!H$3le{24<%?uCCy+Bn33?FWzSAwC&@7=L379>h#Q zFduxl6}uO{m-VZ~&x&IV`Nho*A76lb$#(BNbMB>lk@yz;)d7r4IsP{s09+ydfDVOM zU1zL+Y%8bj60^9`CH}qfz_eL4d#$FvmCfAcgUJ-dDMS0OxTD`!ziU1x&h?ioZinNb zgIp4W4Pl_E?8S*+%O_|lTfDS+_c+JD70rbY{M*p~({2UD^Z=Bd)>&`!c>a7UF*wc_ z_($##Cbs+w_(uk{X&U~KUs&(?8|>T44mHYt{Y6FNovCB9qwSGy`j@T1IauV*1Ad^K zysLI;&GE|z;9q~)6H|XAm2R(Xde-1n#M(G_IR5z2UQb)(gZ{fx2jmOn@?=bM)fn%Vo9qK(HB(nD+n?s&@Fm%m)X3CNazaG|&R z>8-vK@TCnzwOAS2(lT-T}H zbbU#;Wrw!72UlF2WZ;U6_Tq)cE5viX*$#|*9;z@w<2a_<(tMxthP{S&VM{x{^?nJo z*DuJ-QfJ$@H~zlaI5!~n!Y4x9K#^M>N1g%!m}7nGFaR;vy<+Us?BLH__sM3^=hA-S zjqj98UsWFbGPH34%i&%UfEfUse|-RcB~~#J4JWH*%6{g&Z{@7B=aye7M;Wfe0fzdB zaWT{VF6mFjsr|weC~;PsEimQPgtLg>3zdL9R}F{8^Dw; zuLFp?>Nke|X8U7fZf)64^*h3y1QPDeaaPUI%L@pHpyFx1PVzQn*fzg!G8m>T2k4ep z=lrnoOCpzH`>7#G$m2JmjG&)G$2K3-RNC-XU~Q~F>SOEqW1}2|ZK6H}xJ4kmWO#1=>x37eR?Cm|(}JOX zz?Yh83)IUNOBN_|mY(z>A83Pn z=g-&){HfpGO}|z6SNcD{RR;gh`Nyv&Qh#7J-}>cm*MMb?e#;BC_ZjO}`z?3s1PYmw zCsx)&|NPy0j*JnwDrF{aWTL6DKC;-Rsp4~o@l{JCl+-mGJCvsvr@Wzlj}Oyj63dmO zdfC*bbZ++?Bl(kKtmOwC=bQ?E$qxIYujG1hfZd)enUZ~YU`b!e+URRo0FSYE)Os^P zr+?-6`Ssr5sOIURGcHQ_s)(L#V|~JK}R= z?3HEUoO8qf%=$PEd98mvSVnbc#=T=(_2W3e$ZCtR;aQMzRfN(r9Rt;Vw>C&6M1N(w ztv=<7iQ(!5l#a~9%ra@en(*mdlW!A1d|k?i?iS`qdnuQQik)TiD_`3+jt9bSNgAoK3iphSPfxG%A0sndZd^e z_O0oqgnY{FCR}XsW4Va0Qc0_m5UT_1!Q+5oVId)E?t9A&F>jC@m=b%}<0I*%rzU3wToDPc07fPcPG z5E;3BdtbcODbf+KD4Bu5vaXW5^EtXH=mKoRvMDkd^C@$IXnV`pbWAK-Z}+raOl9Y9 z(F_LU^J&9~=&`~w3Pqh;xr!1r@BS|m+STVgflWJNX<6pY2Df&PoDx;7zXHh)ks3aY z8-26Y_wers+#49AAGJO^*U9|K*WiYKQw;I)ciJkm`04hGXbaWw@TogE#GU&69%VG& z@f(_J!oPl>Hm0qnvXeh1OJ)6JfGoOu?5uPj8v)R`r^j6Co^51{80*fm(`F{kV&7gr z^_|;%|GxJlkNEihV#4u-ypvcZozy>@*xP(ebG+qe{nWShKY{2{KV4tc_gFJLina;> zQac=c{H*XVf&ec6Z}uam8g_P^?M`{%-%vj}@?jf4R6U-%t=RQa*3+k>UFVlK{iiX9 zd$+|@HvB$=SiZ*R!-ntpmt*jG59N%Bj48vS@-KPM#6t%seI5Q6j_A3T=VwsPd9>pH zGZAw*X-A$38&H06&;Q1Jt=(5{2IXcu%{=jje@2wKRymVM5qkQIg`%Ib6J{nwwdpLE zox=NQ-?f?{bG%x<=6L8%<@u=;lb-&zzGIbP#js!Q<1-)3K6?*e08qg1l>N2W4H!uv z9vGkb=@jQ_xxbBdBf&sOtazeQC&)_xqEj24!pMs>lS~7dkK`SI8LZzW?ef;)PICe$ zdoIY+1}Ttl>Ob~Vy#i%RkBzcJ{SAkFz%HdU{V_>K-CBI?T{P&^gYp~7wfHC^%sJOFz^GH{ z|J_G7=u%Q(GWBIf;RPW9-3x zpYlpH!{_j;BDuzQn+k|E&$d{|-&H*P?74ByGUJ<$vc$B^&qDEEWEiqV2?u`QdX!F_ zaVfu`Pi7tXslEW&dUMVEEZXyq;H-2 zcziGl|1!!xYYfyzsAgS+yW@u9HB+^7+|D+NYx?m%|2mlKem4U!A9G<)_vv77{8zs1 zk80ERqHNa>j+I|*PGxng$@r55m63W`4Kig|-?(;vZUl9EW)hO~A4->zs&nLqK0$(p z;~d#H=jsOzk>j*YNw!-mkd}-BItwR8eGCLZqH05Ofrb#6;3V=8Vah5=1_C#dM3P^ncyM2xt|n+pMuIhy zl)Oxrg{57tKM7-c3t7Y)YFE1Tpn*DHRwy3DhTQ zn1RGn>z_o`5^63E`}^;L{tsa=rV}KyHJn?UShtC-SD+l2%@|=jE)bTk>NZ>KZI7`Y2h{q@Sm|?R$K{D!f9sO=SzN?G z2h3+>Q+dO>j78em_@i*m4>zT5AeHlj8wX-Dbb-bO~0jgS3_DY;PE73KZV zzCwi@2mA`U(WXlsysx@t?&tWbvcA9aw-cI=T6+j!9moI(YR?r2j4MuCg|8oM#;__( zoBZ9vFW1;KfS&vc4SvCxB&R?=ZKfUnW6O>?W?z0KdtP2e&DU%`vx-}4wRSA$e6?`_ zz6JE=CVPB&!}tj4pLNCA3tW=@mh;7lDT$HZC&w;b(1vTZpE`PEHz(-7F{A901)UH2 zK&Ahp{>gJif6^81e{d!Fn=zF>S_0O@B|}cy@lQKA3C)eA`Trvz*Nizvw^G*k17qei zb+DXj!RE}Bs&dOdy_|Nha*DR#yX;5Vw@jiJkKi8S;lZg-f38J1{^dqLJ+L22Gb)*l zNJQnxSlDzfFKhgd4T1av1~oq}oCkKK4z`*4z`6Zo*+~>npVGgNA>yL(3?QgiiHT3oFt?1!g*f{k*R7so zK-;>s}w&gkuRP{@9tfet*AG#ak4C;Th{wTYd?9{5bQ%c>1#n` zW7s}nu~*~AI7RQ)#mo7xA)>h>y3j8kv(_p)Jf)AD|h=P3j?#a9k>IGu;k z;JmcQQSXMGJg=8UoXa>ZSD6bBoqwc9{j~e1H3ysmJ!f4*8T{M%H>a}cK8rM0<>!)OBh%@yRiU z2Y?LF^s7xQ1?~+n#L+CSA!zwp*4G$Zj)5;~ijjllk#`7vLp1n*A^TkaD*%6w%P`lCQO{BOJ=iy95)>Y`c?8#JwQa;#->VV{`=62G#y+khve^#7EaVRSB)@Sw+6|Foe@?+G+i1C&tC9F(}J{w+G_n-9(gV)&02}m|U>V_6IKSS(f}lj95tg zTaRx${;lZZ0Uaw z;almyJxQDK4}S1otXBtz-q4QuNDbqrT*#(qG|MI+XU0-R1MU##IbMf8ZAe2suF3 z!Y(jV*gPBk_=VFZ{8e1lTl)u}%5E(mBmU{&L3Z-}Z}$$b|KIBW4WLdm!WQ*MMX-|* z7<3)=5fM#>XVTu^-|BR7(FH81#|Ub6v)Wv8<^*@?Gw;qSv9UuSZGH zZyIaamjDq{N9eEqJncO@$L0Af^V4P4E~}T#ef*EUs{)0D^!ir6Z0BDB9AaJ;+O)X4 z*Oz4~_7mN1pSv|hG}Bw1-R|tvxcUquzdGh_cA+xOc?0zUrbw+jfrM3`!#A6c%LF^W zF9lE7=ZNk4SDVQ$e;oi8fml4%kUD)%Qc>SWhu$9mP>D(D=&_-`pIXM@biKZT`D?DX zIEy&k?sQXsn}36##CUf_U}j|5iSWXaRZHIaVHWT!0O$tb#rXKDzPsMcGZ7bV8C>}M zW%4rVs`1&E;!I^0NxocMR^pBQgnjRF5wo1^%o@k_?YZTaoZ=A&#Mhh=VyHs&iZ zxkX%usong=*ez!~cxA}{EZ_PRw*2ggnI4E*9#9%b+BcfY57Owh}Cql3KWYmFnu{jYFcWj|tou<=d*1v}^bd!O$o{uWn1 znRxqDf5xaU$e~Ej73~KI)?bY(lBUhN4hKHZ=xEQUk-H0Zw>dZce%<>|m_7m76z&n= zqiloZwQtsI%<4F>lG&+EZUgzm4tgyFp^Ou0-^@3lA1v4p61X8JIaE?*MA0_+|6grq zw=F@6Dq%hE|IVI^68Iv(s@**^*{gSDn)pKq0cuW{*Faox!vm2A3QYaZ%J-k&77TkoBS60c_nM3 zJpD@)v5x%yaV6hfIr5O+ueP9%PCTiaKh5{|Q@jY?HT}IkiA~}FYdJ3M>imZsL6R>$ zF>u^CJ~7v)yT(90K5XFmTA!8rQyiW=b0XQH*yC{O?1`8%$-D_1`d@}*EV1`~8p%zA zZwXpFXKecBh{M&mcZwsl`A0iF9z^^^PB%nc*$BQi+xX86hdE;EfOs^($vfN zQfnt*Ev~Ts5Qn^drzRQF&}){5pS1?fuO0~?>k8H>bNc=AHhq+a%a!Xnt!@ODZkqp2 z{QqfN`Bco8alwYoNocV~@({vh`JYeb!|y^r8OHFQdD;|fXeq>Vt%?U5n7D2)a`1zn zmd7~{i2-}O>A&Q^HWa?M+{AfTsy+CxX*jv8U6%gY8m|=FpJM|{&?`+CX$Ukp3deF*;Qa2mj``_J?JwLYdV1NL&ocFJnIA0s;F zemZeVCP0n*3KD0jnN?D>ImozSa^~67wohV^TtF*Uh*7takN!oVm5kJQ_JRtx+DQyD z{(k;l3wVJ0Q~h#$_9>0u4om&r-)9xz6XX|4tviQC2p?Li!Bw&npNO51NZI%VODHdJ zxN{kDK{jD?lKY7oJ4uo)uN6)UN9DVvS2;Wc$T?I0WJF|O*e z0!(V`?tE);x)@;hsy0LQG^(#88BK^8<)R+=t2kz5IZXGXe-c3P-P)C$Wk9~V zDErme%D&a(j^WDppEl0;?DttFt4+vsss!+LAN32ivghEtnCyKGfM2;B*q>Ox`RNXO zyWHXN!PguStEeB7jvLu5#`eX3a9}5osGq>C;J>i!ok4D}Ta%j>+tchu@!!nbjwB5$7WRg-_~xZ3F%vy@LN}i=eua zp&j}3yZXdZRgpRshjp~CQrz+C001BWNkl3Hv30#5Ncr@TCjy}_=SmQ%q#R_VZSdvJKqyi-DKav9TAH`yY7R`WzHRlb_UTQ)r6Zq$i*| ziCN@V*(=K9zGW=eK>AKej5@BE=q>W!FOso8i%Y9fWKUa+Uq%4qUwC$$DQ9^k)lXQ+ z0rh9Q`ZWjsLRcSO>oTFl9Y~U*;nhEfIw*&n;S_)6Gb$3vas8E0+oHpaSBYA29U*x@ z6!NuGD4oc{j9er03^~81&&VQ#P4{9uAJFb}Z4B*{{;3n9*`hHWeQpM+Bm;)nOD$eBT*i13uLmD=Im}bpKRL1+K)F+&XY!xB-jSGs z=bWeA=D%a=CqDZAYxxw9exNd4id27BIf}*4EGw>?WK@60RO7-JCn7d676mAuPFlPf zuSaZpVuP47d;d&VE>pHscr7aryZ>(f5ZHiXa&+m~?g#$ciH{CW^=D4@%GY>^Gt9s7 zoyUdFhqIi>nwsMTeRqHYnPspw+H}ZzOp)~qWM=(8jeGeNM^;L!h1Kvh&r6xJmL^7z zAS?c4vFIw}smofutjqj`|FVl5qPuy?>DXVf=9d99s^o;V>!}>_k%|?bW4-vV^i^CY zoWeI*FWRj9_gcso`um^#M*z8-<7GL}H3hq5H^*K(+12`XH!8hQx^Q+WLZ9^Oh5OLi<@xvIP}ka9 z>m5qsGzPy}mcb=QuA~#V9g=%AQ2Lhs8cYPsT&10#;w7zrZ6oD0i@(*{&Q{*`p{iyng9E4?VaPv z9`_2MF#QpY0SxUv-Y2Hm;RpMtEl0vc@=JdX|HM2XA$55INAq8h<3$=)`8MfrySkc1!skNMJF|2QqW+B<0 z9EP7iun;gE%&qv9Pa*@UlynrA;nKE zc5B(4vt2VQ|)0UJq@Ct;MG-tDbr-X5R#)-us2<|gpM_;h}Aa+YHG!vJ#5 z>jaG>s2l+73I2=v!GJaQMcNU6GARum?z4W;un0fs&wONK?f{n_R@miG+&Zpv!ifHK zc-$t8*ii4*s&P=6HqFAQq-?pNj}Nf#oZ=8sd;gBe9aS2~a7@`j zrqKfdPw>fIYiOhDE0wN8r*p8@E}(aB{=xS$OzQhVU;8 zAcEU{&{MO8dhYd|0s4ux#R|^2D3Y&Vu~iWgY(?3w}xeTKxBz@g@3MSeoJu?wLdu0EWw3mi&_U9xJYLJmjIDwdX(Z-z2-%{Q}_M zUh4FJloLl1sT4?gB&n2ZL1zV!JAv6des>1NXuF!%otmx$`#-Qg5NQ~JprFQc@Gkxy zk+%~X$PDU(^ozEy`l-vlbwe%2gw*03aX5}JIx6)8E0P1F((P<*SKo+ir${;n0$Vy{ zHlqNXQ{@s2_68Q8e&SRi0%ZK27}q%&WOnN*#f zc8oy+M2>&webk);%q=nh-Eo6)$H`V=b&V;5;7yhne68x`psN#J(uUTe2njVuoJ1ev z35<$Y$|lNW$&G^ZFn40cr;yL`gMhjVb-rKxrJSzLtYfun9}|O4dTq8FT(*nHJAgf zb)qhhGnQ~-l*?oM%&R1)H_AvF`F2H} z!1_cisJ_yNFL)WtU`UnQmD~b`U2W(vdZca!c)ZR0SFDo!M_&j`%>eS+u(kRUZK0mOImlKf5fI`++p!Q+dn8_<$rtm3Ci^DSJ^*(AJ=pD*WUFn2gnRr zV+m{IVkJbUet7E8+d?`~<6l1g_WsbO`Z$jdv8i)=-8F1c#dY$R%<#UdKMlYKwoK-d zXk+QR|9*`w9(*Kmc1ampTAW~6quBfY zF6MX8ALE?$*Y@jly4w1d%dB-ux7$qr5+M4YPiRD+jW)ti?dm7srtaW?ZvU_AT(Er8 zNWX^Yb;??C5=-7^x#)n6mAE&5N&~)Ng}i@(0D!uIOz+&d2YvW#78+>H;U9%yP|=`a>3;=Uzr2rNt5 zc{ml?qB>ubw8<2ne5y&;{P~<(kO#-<-CFQ7>biPRYF`Z)8*jEXBzPkLhS$r#&Uz8( z5UYluu~FW!ou_4buT!R8+XJz0?|r!%*{j1%`tJ)sB~wdCR~9Tm4v+d`N0#Jy;`b}4 z%*lmf^ruVWsV_!qPI<48{%!O5Tb!BN2{~wf_5Wi3SM#=@`4cIG801F)mbB6NAKW^a zo8&x#z4GWte}m~3CMtJERWHN0_~Ks3cizZ9;J;!188g^lU-%xyAmeWL5ywq=7NPNh zmt6lMhPI=^u*Mx9;a zjtkyEMi;>|rqT4^5&^bgIP1TcOx=_K^4*n}MTFxtwR4KWN`Gx@Uf_1)ec{zE_>Sgp zm==sDDcXT|Y#Yu)od;DnUlE8Z<_bNVZ@M3dPgsXj4mNEvm;NAjZ@a~hp8=rB39Pp* z%{e}8W&S;2`ZHdn_R|J(r#hWO@VxbJoBpw-v zVG=0%&+4=NF@OwhOqhuMQ-2F0Y`f+AxRNXf3BH>a_1t-Og6ctK^)d#P2>_>-A<@YA z!Z zw0+sgUYvzcI#C<$p(bL8laHF+a6&Q4O>OtG9*euFbMjK>uz$QSJZw{dMl)Vc!uqae z?{hbA65s^WVz6SnP^L+l`%qihF2#TG{f7M3y5Eww=QW$jpRQ@+`L&tCxm>He>05uq z9kB<$HyppM99ZVOIlXrye(AvHE8n?tzRdhnuW$OxyR3swrU5c(;n-eI*>I{h`WI7} ze=E~mjDxGTczw&y!XNTJW6NWMz<~xP?a;z$arZh*S?KOliX$83z{-?uYlW%KEB{L_ z44fJk9w!vvBk!Pa&ifQ=Seyf-11#r1$A+@q|1DNY?lQaXEB#Z*jpfCy5q}cP{cPhe z0Z?-&08HZFzi^sX@#1GOX&dZn*D?Em{I!~>Ua7k%T)`>tcvo8HlHg16-$cjExTP{hd0106wVZ}Tbx~h$bZ_U z@*`zaFSf`2CVr8)W5&n@3%S_tRlf*HvjILW?#dTsJn}QEzfitlThFPw1z|t5MK%B8 z`!1_-sW-47j0ViVLhsrX)=g!WFP1eddyLTepG;hHJ$;Aawl=X4WxW;vt;2y{B#&a* zogm#TUyYVsb|s!=x@rr9{XcxMFIXQwmCC7}ebeAPa8h4O{fx)NKOGB;CocyON*m&i zTW98GMhBd?mxn*-uMfxv45fUXF~{MO{pdBdhggvM4}O<=Vw2(cT&&gN%W$>ztiM0s z-u{DYTJ(QgtoQoKbDD5x@=;czO4SrsU(15CZz_23U)r8C&uBs_=O@!qi^TXBb8J|w z>%`}X1N7nbAYHFae;FllvWnI|qO)%hxS%jbYI2Zqujp8Dl_&%E3NjYtiFLiIoP9*wWP#Bg`@(y{uU zaCG|R&`^i^v(5we$^0?O(h|o zT~qQlCvvu7^y_kpfJ(yQcfjQMu{q3x@(o;6{aO*l`i#06QVn+V_duLU>6lpaSfTY>+(ZolV$W`K$(@mg^#xi0z@368si zxpMml|NSyX$6)s0>~hEnoRVL0MpTph7yL3Mfax!5jw@(S2GJ4#6b?RE<5vEw>9hG0 zoHR~&mZAFpIB=L~lz#FG?yre+nd$iy3eEmS|Kh*U&NmS%5B8zm`luKFAsYb_0Cc>y z8DxY1IKWzhYcl{eX3qzb!v6*8*ZIdn_7!K-z6ST4VoJX(zK9m3qi&e|^0@3L@G5?PLN=${vdZ zuQ%6G!u`f4?nLBS?%KG)lbRU zHHg$$vb|)Pz=RtCHvJniKL1R5atx#S{0vSUW$jvOQ1BD2j!QK;Q=sn}BY=JjrbBtx zpK2l^6kyfhbM>GkdRE~sygCJ(=zQ5TRHZyG2_`vyMbOJ7D6PkoPLAl;%gKj#o~TV5 zA%*Ey6`DOUsB(cnCrRJc0-PWXQ}`RN(84VxEnPVala-nQtU zB^Gal=hmZ~FrbQXD|B}05Cqm#uH=u%mB2*{<6+CJ)uAEr`QlWjS1lDlRC+-0Tb8r zFZzdZhoZ=TOaE%oPpOGf`kLh?0 z*lc-U7?a6d)uTL?n3yu2|J`yPDzrKd=FyU=z2wi~mAh*&v@g)5XAEiTvdVO_+E;kZ z-&oF`SNF-hj)~6m=#r=D_`T<>YVt{oKX-PdiH{QHBUc^3xy;KKr4uG0uPb1S#%? z-`(u2Fe~152TIj%JB$@jFMkt-eR14ZFLszJ2ch5l6TrZG*RBpzvIZqHy-EJvsq{R% zp7;IdoKqxmqNj7q1CMj@`uxLjB~!(aMc*TMF?q{-!+KDmF4n~y?6onRUBA1Y6OX+` zPBtNc^}(Byc&+@q>^}NR`%k-qKg6GO$qxW0jPL{CH2shd(E@yvj#EP#bm`pVvoaet~k@n2Ka{+yp*kmn!3xhBKi6{6s^wVd(!t^)?ee?fh zf0Z|t@x{2>X)fMHJ1Fesf?=><#H!dt3I@cS$KAkxpK_c1qxTVO!qzH&F0^y`6aK{9 zhqg=2vaR>@cfa}KV$*$UXo*7q5K_nyme}$!NzF{%V}HOvu+tB z2OX9BxJ>IzsIQES=#c4{UsSB|7;~H;dz5t-l{gVCMEA|^QG5CHO9fpm5F9VRs_xh%=Cm%Z} zvqfH6>G@=ycirCWDzl`{y&hv1n`Rsgxpa;mOX0ZMtL`>?Kal0n@;!)9$fDl>`A9sbf)PH>$5N*i`w{-b9K!^0WL~8_Gk2 zb9!4Z8QiCigZ^odyZVhJ^|cY(#{R@rlQQ)!$L4in@xluyp+AE}R!aHObq$lg#&*jb ziy!ps2tA1jf6EbT99{<8f zC%ON~PeUQD0>99~9AmysnGRZd9eyX->FW49|FwMVTW+@zSDOCHfAST;S~8WlNsMPT ztg4;`8e>ek+3e&Hq{ylpXox&@=whFS!4n@7*rYzW{Co z;K}dt`ka5pm+*bsWHc;o#o`Y9e#<}578VtMDj|`L>J>4kl;Wvt0_y9bU!JK=x5rFCTgxp#kgv`#%V!hr2(W~$9^eG>|>q|%JwSmuQ?|k z2Qjebm)*oyXJsE{b2YyetD~PHNPaPp?QYoqF#R54Jch)5TNEvnAIhaVo}eI3A2kW6B+C<}|k>jt1=(4mf<)_uDY1akWOHQ2r z1n7`EVbI(;4{TO52FIn!szb^12|-4v-5M7hndGKte!~YN3Hc1IB>mbIU0I^<*N9iO z#M1>SGc~20!+cJ^F0g#}Ie!JPFTd zzBn#XY^GYWpBwQhS*Wm$iY@H_`_~aW+h^My6DsHJxy4TL;#>J-S|$6@N#$Oq_)V5^ z_VWxYddRKBWXXM#E5|WBy1{5%SSC9#VCp)Y)`eJuj z!jDT4IoVg!|ANI$KE{+OK4>0hC;HHTJ>;)Id!CF@G5Zp4yo~{iZdty-Qr;W+FS@rj z&HP*ZpX8!Ft02k#D6_1gQUK*%SYLqXz!4R%e=|dv_-~`6?4|SHr)U5Aq~q=G%Txb7 z$q<9dC1&;i6kF!>=M4DN`xpO7+f#cNpVG758T$hlS8_oT9Xkf4FFu|=rLg8Gv%`(PwMIG-;?$IeLU+sY!VDIqy9hM z`}ODl27vD@qad}-HumQ}j5v)t+vD?2n}YgRW@lOCAoZ^5tc%Uw)&8f^aMYEJm-^Sx zPsyrd49>Ar8w*Bxo&Zc@FF3{=1b?zdwzWh04Eo=bzLM7-&m7Lm)SEWitSFxG(={Zlr!yX-L)kln2kQx)3Ae+6G^W7jNn2dilT0rC+cg zaKjE>4h%2<2@l=-%+(l?lFy*0@!QHod5JNWj%K`?0A@Mm>0Q~cz*@0|&%9drk5ILA zj&mrG2?WWtA(!SKHk0O$yfXBOMu|--m4hkGE+!JsQ~!t)#(y7!G`fzjV{HBtU&ZS) zl#-<%T@bEZ#i1J4wLSXIDH$2q6mCcv@K=$O{C6&|JmVCUkvAZQBp+noH}AUg?D`zV zAaXpgn_zj`ATghrR|NrRz73#TP=Hk$K+Sd3$sFKtL$VIkLO!@p4zSkJwdq3aK>q88<}G$F zwdeAgCG{x(>-tCk5kQV-)1Yq0#ZO0mDNDI(xPy>Jw*rha*R{kg2x|ers-hFdv57Ir z3mS|+pE6kQW0h%E@F{L$mmFshN(7R4FTN>+9`WEb2c>JoZ6z$!mXS#H8UqU-qutOf z@2klQ3y=feL_QYwS>ulR;2o>)+h6GQA(7C(1C}!VUvbKS;9_&FyZbTSk@)V!qhJerK4562StnXupp`#mpxn{t z$~ntq@6O|1(N{HXu`I_)--sbjG??#KHL$BG!6W*Y>84*tAD;ZmfMKL-RZz%G#8jG5}l zDN^|Qv@l~scdYFM=nnp4KN0A4bD5jGY+K_CW8JA|?Fh<(b6K8o>}u+Kl4UWT1^r{p z+s0N`m|sgyZ@I_UYG0pA{~MlH6)4f@Nd>V4|J?fr!5%K0L%$>|~7Gq}l5ks?-P zq7R};|GLAF`G?PlZG!)JQb<)N^Zb+_PJh1)maHy`-){FcBUrGVDhpZ0AacW5Ys4I}H#kDOlDL6vMv$Iayyg)mN= 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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") }