diff --git a/Common/Locked.swift b/Common/Locked.swift index 206b2a5..849328d 100644 --- a/Common/Locked.swift +++ b/Common/Locked.swift @@ -31,6 +31,7 @@ public class Locked { } } + @discardableResult public func mutate(_ changes: (_ value: inout T) -> Void) -> T { os_unfair_lock_lock(&lock) defer { os_unfair_lock_unlock(&lock) } diff --git a/G6SensorKit.xcodeproj/project.pbxproj b/G6SensorKit.xcodeproj/project.pbxproj index 1fcbec7..7fbbf6a 100644 --- a/G6SensorKit.xcodeproj/project.pbxproj +++ b/G6SensorKit.xcodeproj/project.pbxproj @@ -7,81 +7,82 @@ objects = { /* Begin PBXBuildFile section */ - 0012909C39D62B9E08C9B064 /* G6SensorKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E1FE9C6D076DA1E91289E83 /* G6SensorKitUI.framework */; }; - 04A2B67E2547E628203F1358 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 429E660585726604E67AECFD /* Localizable.xcstrings */; }; - 04B8053FDEAE43229A81457E /* G6CGMManager+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3CDBC2872D089CCFC608914 /* G6CGMManager+UI.swift */; }; - 1E4F189DD2133923C1AFE92E /* G6OnboardingViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A74B38BAA7E148D962602A /* G6OnboardingViews.swift */; }; - 1F73E2F944A55A9AC25A90D2 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8F0591ED59D9FDBBE9B979F /* LoopKit.framework */; }; - 20ADEEB69E1914A498A8288C /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E317B6199E7FDA3530EB43 /* LocalizedString.swift */; }; - 2B177BC4AA7B5AC20A0C1CF0 /* G6SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3226DB4412DBC72DFFA90E /* G6SettingsView.swift */; }; - 348D78E88E7AB61A3EA8B5A6 /* G6DetailViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F3FBE9FF2D0785F9F703D83 /* G6DetailViews.swift */; }; - 3B345FE82B0B128320B404C0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = BE9888BF3A30CE450E970741 /* Localizable.xcstrings */; }; - 3D9CC651E9C531C097620F89 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 253838547314B57604F7AD40 /* OSLog.swift */; }; - 43A3BB61380CE04F8ADF97F5 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73742E0944319E3A7E474AB9 /* LoopKitUI.framework */; }; - 43CCBACB96EC5B4551CE5E7A /* G6ReadingDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC5A15D4A759F997F7FC541 /* G6ReadingDetailView.swift */; }; - 44180D8F790DF0B7AD6858A6 /* G6CGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED4B45A9BCAFD7FD14DE50A /* G6CGMManager.swift */; }; - 5124B64274AC3FE6A2C96D55 /* G6SensorKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7B63810D31CCFC9F7F186D2 /* G6SensorKit.framework */; }; - 55F5DF80BE7C11261D02F92D /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73742E0944319E3A7E474AB9 /* LoopKitUI.framework */; }; - 5C0BDAD9F8F46B1894220D75 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B534C9F9F094DA2E641E1F56 /* HKUnit.swift */; }; - 60CA6A4323498F0AA5733921 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F64DEB4B031D1C5CFE809317 /* Foundation.framework */; }; - 6B46867393D9926E754A0B1C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F64DEB4B031D1C5CFE809317 /* Foundation.framework */; }; - 6B717441841D031D0B28D767 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F64DEB4B031D1C5CFE809317 /* Foundation.framework */; }; - 75FB2E8B0FA7123C2ED8DAE4 /* G6SensorKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7B63810D31CCFC9F7F186D2 /* G6SensorKit.framework */; }; - 80A0E78EA46BE2777673109D /* G6SensorKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0E1FE9C6D076DA1E91289E83 /* G6SensorKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 8BB678C1F2BD1D88FA1CFBBC /* G6SimulatedSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74783BF907EE99207B30B578 /* G6SimulatedSession.swift */; }; - 8FF638A85A7E67CA3BC5D9F9 /* G6Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03906F90369E7A2BF4DED672 /* G6Alert.swift */; }; - A69AF218C10E883AEB62C3BD /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8F0591ED59D9FDBBE9B979F /* LoopKit.framework */; }; - A81FAA3A42066C630B773A7A /* G6SensorKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F7B63810D31CCFC9F7F186D2 /* G6SensorKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - A8E3935009CCE36B2536A64C /* G6ApplySensorViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76466FB9DD611978BC655632 /* G6ApplySensorViews.swift */; }; - A9E3573682AB3B40EC374CA7 /* G6CalibrationGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE94E843AB267DF79DBE2E3 /* G6CalibrationGuideView.swift */; }; - AF6D1CDCC2147D486CC666B0 /* G6AlertEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58664EFC566ECB3D78CCFAF /* G6AlertEvaluator.swift */; }; - B379B66CE81079E346AC7C74 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCBBEB66782BF39C6C015CB /* LocalizedString.swift */; }; - B62C7DB071276B15C354E424 /* G6ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5694A019EAD8454DABAA13 /* G6ActivityViewController.swift */; }; - B920AFAF7C31F1E389D677D6 /* G6UICoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB91998500832B7983C5050 /* G6UICoordinator.swift */; }; - B974B8BBB65603583E2AA9B6 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89D79C9A64B80B4CC5B3DE78 /* HKUnit.swift */; }; - C2A26CF0F1BB14468B041B94 /* G6Graphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD441CA0C87A3361D4ABC996 /* G6Graphics.swift */; }; - C402E99C66F01260013FBB1A /* G6ShareCredentialStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C22FE5054AF7D52948DDD28 /* G6ShareCredentialStore.swift */; }; - CDBF362147EF86005600539C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FF491D549C3EF35172BD575E /* Assets.xcassets */; }; - E733B5AF9AC6DDC38B92A330 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8F0591ED59D9FDBBE9B979F /* LoopKit.framework */; }; - EB7246F1D6D512CC2D28B53C /* G6CGMManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFDEDB952EC717B78C87578 /* G6CGMManagerState.swift */; }; - F605B5DD6F6B98C7BA18BE26 /* G6SensorKitPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7985708466B1C12CA524C462 /* G6SensorKitPlugin.swift */; }; - F63FF74AB43CFAC6FA5BB9F1 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2737F7376B1AEBD38191DE68 /* Locked.swift */; }; - FCEF70D7853D669835A2C2A2 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFF2008F7747472D563331E /* TimeInterval.swift */; }; + 068F65E11AF1419388CBD953 /* G6SensorKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D531F0A53057A0235E7C192 /* G6SensorKitUI.framework */; }; + 14589281DC7760165D5590E3 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B0799EB2EDF3A7864F7C5C2 /* LoopKit.framework */; }; + 1BFC8B0012423024BCEE84C4 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695593AE5C331A21B37C828 /* Locked.swift */; }; + 226AE1C008E3432034691E58 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DAF739DBBA35C93627B2A12C /* Assets.xcassets */; }; + 25B13BDAFD609EC61E01ED42 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CADE8EBD6A8DBD34E5F08E /* LocalizedString.swift */; }; + 2921FB176CBA1E3FACF8C998 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8C69491E1F3F7F01BACF348A /* Localizable.xcstrings */; }; + 2A9D142A81AE191360B058B0 /* G6Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC51720925E26FFF9526D8 /* G6Alert.swift */; }; + 2C77CCC0A4FC873D451B1D9C /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2803D463F94F749B9B4C4480 /* Localizable.xcstrings */; }; + 2E9270FAE984E3B653790956 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B0799EB2EDF3A7864F7C5C2 /* LoopKit.framework */; }; + 332BA57D80F0D8A629C0881A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F95502372EC14A32117B5F92 /* Foundation.framework */; }; + 3342E51E140EB78C3406DCFC /* G6SensorKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B421AE8197339D306979939 /* G6SensorKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 39603B3649182CA37CE8237A /* G6CGMManager+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2C05844010D28B27D4E02D /* G6CGMManager+UI.swift */; }; + 3A518597A56D377383B5D384 /* G6CGMManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650CB121B3A64293DDF66491 /* G6CGMManagerState.swift */; }; + 3A7D53148400D95D5EBE9B20 /* G6Graphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC82866E887A9C466610864B /* G6Graphics.swift */; }; + 3B2B08D9C8ACC49731771581 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B0799EB2EDF3A7864F7C5C2 /* LoopKit.framework */; }; + 3D85A309CDAA70C948DB7D06 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C199CCC759EB31B7459FAF02 /* HKUnit.swift */; }; + 405A216F8D1752F1421D4887 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4847E689C846CC527559717B /* LocalizedString.swift */; }; + 4159FE6F9D1C67AA7D355705 /* G6SimulatedSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFB4FF079E0E059B0A2514C4 /* G6SimulatedSession.swift */; }; + 51AA80EED323BB1051CCF352 /* G6SensorKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9D531F0A53057A0235E7C192 /* G6SensorKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5623B9849DDAC97251E7B413 /* G6CGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB56C95B7CD81513D98B212 /* G6CGMManager.swift */; }; + 6C6A5725B781F8EC0283A2E3 /* G6ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20A6B1AD620BB43E52CA138 /* G6ActivityViewController.swift */; }; + 77CD64A55933B10835781892 /* G6UICoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60451EC9CB88AAED72C7B4C0 /* G6UICoordinator.swift */; }; + 7A17394DCB757B79A35A9401 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93F66577CA895DC0539B94DB /* LoopKitUI.framework */; }; + 7B236774AE21607067257858 /* G6SensorKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B421AE8197339D306979939 /* G6SensorKit.framework */; }; + 81CC3E50E4370774F5954A78 /* G6SensorKitPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15C3F478B37D28F487EA970 /* G6SensorKitPlugin.swift */; }; + 83FDE18A177B5C10B0504485 /* G6ShareCredentialStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DBE6B77523AE6CF47FE179 /* G6ShareCredentialStore.swift */; }; + 84E98310287C4DAF8D376F5C /* G6SensorKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B421AE8197339D306979939 /* G6SensorKit.framework */; }; + 8587D2CC46C9C9CB94B2D3FA /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = E067B8007DAFB29BE2F03F48 /* HKUnit.swift */; }; + 85D8C77F66A1FF1AF57A8F17 /* G6ApplySensorViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5C11477C372E423D1838A5 /* G6ApplySensorViews.swift */; }; + 9068DC2E9303B2620EC57AF2 /* G6AlertEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFF9D322E96EE881F0910FD /* G6AlertEvaluator.swift */; }; + 9068DD5A9EB5430807D21AE5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F95502372EC14A32117B5F92 /* Foundation.framework */; }; + 934EFF804E6452CD19F40C04 /* G6CalibrationGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F99764DE6442A33728920B /* G6CalibrationGuideView.swift */; }; + 9D23800E9E278C62602AAF06 /* G6SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB76778EA2C3FE465AE8FC8F /* G6SettingsView.swift */; }; + ABC18A66F55CAC750FAAA781 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED9872057F9AB73CCE989CF /* TimeInterval.swift */; }; + CF81E72F011FD1971AA15AA2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F95502372EC14A32117B5F92 /* Foundation.framework */; }; + D48D98C13312034AB8C11BA8 /* G6DetailViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB63314B843491AD67FAFB8 /* G6DetailViews.swift */; }; + DA3B54D9184C892B5F9A7B96 /* G6OnboardingViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCCCBFAEC5AC32DCB17C0E0 /* G6OnboardingViews.swift */; }; + E5928066DDD984FCE765E888 /* G6PackageScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8E7A171900BF409A482908 /* G6PackageScannerView.swift */; }; + EE9148D1635A031059CB0474 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2202DFC274BBEC075ADF5E /* OSLog.swift */; }; + F3BF96B43DC44EAB997C9556 /* G6ReadingDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9773F6BB69F46104A715B09 /* G6ReadingDetailView.swift */; }; + F5B695198564172CA054D830 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93F66577CA895DC0539B94DB /* LoopKitUI.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 393AD5FC1C4D642198DBF3BA /* PBXContainerItemProxy */ = { + 1CA7CA75216CC94C77001C8A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 9254155A3D36B6309735ED9B /* Project object */; + containerPortal = 05E8929C0E445F2264A8A045 /* Project object */; proxyType = 1; - remoteGlobalIDString = 3ADDB0CD055A33C01C7B54FE; + remoteGlobalIDString = 8F3461A960A54D2320B21FE4; remoteInfo = G6SensorKitUI; }; - 5DCC41D3B8AE0AF30CAAD2E3 /* PBXContainerItemProxy */ = { + 8C5FBFBE5966E33ED892663C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 9254155A3D36B6309735ED9B /* Project object */; + containerPortal = 05E8929C0E445F2264A8A045 /* Project object */; proxyType = 1; - remoteGlobalIDString = 1B26058BD31E40741B0058A5; + remoteGlobalIDString = 215CD1147465A2E980A94D63; remoteInfo = G6SensorKit; }; - CC0AE83086E9B970A7244CCE /* PBXContainerItemProxy */ = { + AB91E4CF703C2153542F72B0 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 9254155A3D36B6309735ED9B /* Project object */; + containerPortal = 05E8929C0E445F2264A8A045 /* Project object */; proxyType = 1; - remoteGlobalIDString = 1B26058BD31E40741B0058A5; + remoteGlobalIDString = 215CD1147465A2E980A94D63; remoteInfo = G6SensorKit; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 4FFBC74F2EB40DC4B5ED7514 /* Embed Frameworks */ = { + D0449C47C05DB7A8F1FC65C4 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - A81FAA3A42066C630B773A7A /* G6SensorKit.framework in Embed Frameworks */, - 80A0E78EA46BE2777673109D /* G6SensorKitUI.framework in Embed Frameworks */, + 3342E51E140EB78C3406DCFC /* G6SensorKit.framework in Embed Frameworks */, + 51AA80EED323BB1051CCF352 /* G6SensorKitUI.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -89,194 +90,196 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 03906F90369E7A2BF4DED672 /* G6Alert.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6Alert.swift; sourceTree = ""; }; - 0BC5A15D4A759F997F7FC541 /* G6ReadingDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6ReadingDetailView.swift; path = Views/G6ReadingDetailView.swift; sourceTree = ""; }; - 0E1FE9C6D076DA1E91289E83 /* G6SensorKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = G6SensorKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 1DB91998500832B7983C5050 /* G6UICoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6UICoordinator.swift; sourceTree = ""; }; - 253838547314B57604F7AD40 /* OSLog.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; - 2737F7376B1AEBD38191DE68 /* Locked.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = ""; }; - 2C5694A019EAD8454DABAA13 /* G6ActivityViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6ActivityViewController.swift; path = Views/G6ActivityViewController.swift; sourceTree = ""; }; - 429E660585726604E67AECFD /* Localizable.xcstrings */ = {isa = PBXFileReference; includeInIndex = 1; path = Localizable.xcstrings; sourceTree = ""; }; - 48E317B6199E7FDA3530EB43 /* LocalizedString.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; - 4F3226DB4412DBC72DFFA90E /* G6SettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6SettingsView.swift; path = Views/G6SettingsView.swift; sourceTree = ""; }; - 5F3FBE9FF2D0785F9F703D83 /* G6DetailViews.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6DetailViews.swift; path = Views/G6DetailViews.swift; sourceTree = ""; }; - 5FCBBEB66782BF39C6C015CB /* LocalizedString.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; - 6ED4B45A9BCAFD7FD14DE50A /* G6CGMManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6CGMManager.swift; sourceTree = ""; }; - 73742E0944319E3A7E474AB9 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 74783BF907EE99207B30B578 /* G6SimulatedSession.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6SimulatedSession.swift; sourceTree = ""; }; - 76466FB9DD611978BC655632 /* G6ApplySensorViews.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6ApplySensorViews.swift; path = Views/G6ApplySensorViews.swift; sourceTree = ""; }; - 7985708466B1C12CA524C462 /* G6SensorKitPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6SensorKitPlugin.swift; sourceTree = ""; }; - 89D79C9A64B80B4CC5B3DE78 /* HKUnit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; - 8C22FE5054AF7D52948DDD28 /* G6ShareCredentialStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6ShareCredentialStore.swift; sourceTree = ""; }; - A9A74B38BAA7E148D962602A /* G6OnboardingViews.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6OnboardingViews.swift; path = Views/G6OnboardingViews.swift; sourceTree = ""; }; - ADFDEDB952EC717B78C87578 /* G6CGMManagerState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6CGMManagerState.swift; sourceTree = ""; }; - B534C9F9F094DA2E641E1F56 /* HKUnit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; - B58664EFC566ECB3D78CCFAF /* G6AlertEvaluator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6AlertEvaluator.swift; sourceTree = ""; }; - BE9888BF3A30CE450E970741 /* Localizable.xcstrings */ = {isa = PBXFileReference; includeInIndex = 1; path = Localizable.xcstrings; sourceTree = ""; }; - BEFF2008F7747472D563331E /* TimeInterval.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; - C3CDBC2872D089CCFC608914 /* G6CGMManager+UI.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "G6CGMManager+UI.swift"; sourceTree = ""; }; - CFE94E843AB267DF79DBE2E3 /* G6CalibrationGuideView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6CalibrationGuideView.swift; path = Views/G6CalibrationGuideView.swift; sourceTree = ""; }; - F566E5E3758DE9D2DBE17607 /* G6SensorKitPlugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = G6SensorKitPlugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F64DEB4B031D1C5CFE809317 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; - F7B63810D31CCFC9F7F186D2 /* G6SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = G6SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F8F0591ED59D9FDBBE9B979F /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - FD441CA0C87A3361D4ABC996 /* G6Graphics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6Graphics.swift; path = Views/G6Graphics.swift; sourceTree = ""; }; - FF491D549C3EF35172BD575E /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 14F99764DE6442A33728920B /* G6CalibrationGuideView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6CalibrationGuideView.swift; path = Views/G6CalibrationGuideView.swift; sourceTree = ""; }; + 1B0799EB2EDF3A7864F7C5C2 /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2803D463F94F749B9B4C4480 /* Localizable.xcstrings */ = {isa = PBXFileReference; includeInIndex = 1; path = Localizable.xcstrings; sourceTree = ""; }; + 2AB56C95B7CD81513D98B212 /* G6CGMManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6CGMManager.swift; sourceTree = ""; }; + 3695593AE5C331A21B37C828 /* Locked.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = ""; }; + 3B2202DFC274BBEC075ADF5E /* OSLog.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + 3C2C05844010D28B27D4E02D /* G6CGMManager+UI.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "G6CGMManager+UI.swift"; sourceTree = ""; }; + 4847E689C846CC527559717B /* LocalizedString.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; + 54CADE8EBD6A8DBD34E5F08E /* LocalizedString.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; + 5BFF9D322E96EE881F0910FD /* G6AlertEvaluator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6AlertEvaluator.swift; sourceTree = ""; }; + 60451EC9CB88AAED72C7B4C0 /* G6UICoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6UICoordinator.swift; sourceTree = ""; }; + 650CB121B3A64293DDF66491 /* G6CGMManagerState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6CGMManagerState.swift; sourceTree = ""; }; + 6BCCCBFAEC5AC32DCB17C0E0 /* G6OnboardingViews.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6OnboardingViews.swift; path = Views/G6OnboardingViews.swift; sourceTree = ""; }; + 6D5C11477C372E423D1838A5 /* G6ApplySensorViews.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6ApplySensorViews.swift; path = Views/G6ApplySensorViews.swift; sourceTree = ""; }; + 6ED9872057F9AB73CCE989CF /* TimeInterval.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; + 7F8E7A171900BF409A482908 /* G6PackageScannerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6PackageScannerView.swift; path = Views/G6PackageScannerView.swift; sourceTree = ""; }; + 8B421AE8197339D306979939 /* G6SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = G6SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8C69491E1F3F7F01BACF348A /* Localizable.xcstrings */ = {isa = PBXFileReference; includeInIndex = 1; path = Localizable.xcstrings; sourceTree = ""; }; + 93F66577CA895DC0539B94DB /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9D531F0A53057A0235E7C192 /* G6SensorKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = G6SensorKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A15C3F478B37D28F487EA970 /* G6SensorKitPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6SensorKitPlugin.swift; sourceTree = ""; }; + A1A4A5B6A02A6007C52230EE /* G6SensorKitPlugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = G6SensorKitPlugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AAFC51720925E26FFF9526D8 /* G6Alert.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6Alert.swift; sourceTree = ""; }; + ABB63314B843491AD67FAFB8 /* G6DetailViews.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6DetailViews.swift; path = Views/G6DetailViews.swift; sourceTree = ""; }; + AFB4FF079E0E059B0A2514C4 /* G6SimulatedSession.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6SimulatedSession.swift; sourceTree = ""; }; + B9DBE6B77523AE6CF47FE179 /* G6ShareCredentialStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G6ShareCredentialStore.swift; sourceTree = ""; }; + BC82866E887A9C466610864B /* G6Graphics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6Graphics.swift; path = Views/G6Graphics.swift; sourceTree = ""; }; + C199CCC759EB31B7459FAF02 /* HKUnit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; + D20A6B1AD620BB43E52CA138 /* G6ActivityViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6ActivityViewController.swift; path = Views/G6ActivityViewController.swift; sourceTree = ""; }; + DAF739DBBA35C93627B2A12C /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + DB76778EA2C3FE465AE8FC8F /* G6SettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6SettingsView.swift; path = Views/G6SettingsView.swift; sourceTree = ""; }; + E067B8007DAFB29BE2F03F48 /* HKUnit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; + E9773F6BB69F46104A715B09 /* G6ReadingDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = G6ReadingDetailView.swift; path = Views/G6ReadingDetailView.swift; sourceTree = ""; }; + F95502372EC14A32117B5F92 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 08CFF0A2C0AF7A01359571D3 /* Frameworks */ = { + 29459DDF79C2F6E84E026198 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6B46867393D9926E754A0B1C /* Foundation.framework in Frameworks */, - A69AF218C10E883AEB62C3BD /* LoopKit.framework in Frameworks */, - 55F5DF80BE7C11261D02F92D /* LoopKitUI.framework in Frameworks */, - 5124B64274AC3FE6A2C96D55 /* G6SensorKit.framework in Frameworks */, - 0012909C39D62B9E08C9B064 /* G6SensorKitUI.framework in Frameworks */, + 332BA57D80F0D8A629C0881A /* Foundation.framework in Frameworks */, + 3B2B08D9C8ACC49731771581 /* LoopKit.framework in Frameworks */, + F5B695198564172CA054D830 /* LoopKitUI.framework in Frameworks */, + 84E98310287C4DAF8D376F5C /* G6SensorKit.framework in Frameworks */, + 068F65E11AF1419388CBD953 /* G6SensorKitUI.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 4BBD731E15A4EBB02214C814 /* Frameworks */ = { + 7892D299DD1C4456611A5D26 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 60CA6A4323498F0AA5733921 /* Foundation.framework in Frameworks */, - E733B5AF9AC6DDC38B92A330 /* LoopKit.framework in Frameworks */, + CF81E72F011FD1971AA15AA2 /* Foundation.framework in Frameworks */, + 2E9270FAE984E3B653790956 /* LoopKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 7034F2C68438154861EB8766 /* Frameworks */ = { + CC4E9DF835B45B2D2994E5A0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6B717441841D031D0B28D767 /* Foundation.framework in Frameworks */, - 1F73E2F944A55A9AC25A90D2 /* LoopKit.framework in Frameworks */, - 43A3BB61380CE04F8ADF97F5 /* LoopKitUI.framework in Frameworks */, - 75FB2E8B0FA7123C2ED8DAE4 /* G6SensorKit.framework in Frameworks */, + 9068DD5A9EB5430807D21AE5 /* Foundation.framework in Frameworks */, + 14589281DC7760165D5590E3 /* LoopKit.framework in Frameworks */, + 7A17394DCB757B79A35A9401 /* LoopKitUI.framework in Frameworks */, + 7B236774AE21607067257858 /* G6SensorKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0B2EBDB688CC9F72D51B8750 /* Products */ = { + 35E638A87A48A6CBC246BF6B /* Products */ = { isa = PBXGroup; children = ( - F7B63810D31CCFC9F7F186D2 /* G6SensorKit.framework */, - 0E1FE9C6D076DA1E91289E83 /* G6SensorKitUI.framework */, - F566E5E3758DE9D2DBE17607 /* G6SensorKitPlugin.framework */, + 8B421AE8197339D306979939 /* G6SensorKit.framework */, + 9D531F0A53057A0235E7C192 /* G6SensorKitUI.framework */, + A1A4A5B6A02A6007C52230EE /* G6SensorKitPlugin.framework */, ); name = Products; sourceTree = ""; }; - 1DCBFFA30E3C8A0058AA3F9E /* Common */ = { + 7CAD88E09B3DB8C98B60678D /* iOS */ = { isa = PBXGroup; children = ( - 89D79C9A64B80B4CC5B3DE78 /* HKUnit.swift */, - 48E317B6199E7FDA3530EB43 /* LocalizedString.swift */, - 2737F7376B1AEBD38191DE68 /* Locked.swift */, - 253838547314B57604F7AD40 /* OSLog.swift */, - BEFF2008F7747472D563331E /* TimeInterval.swift */, - B534C9F9F094DA2E641E1F56 /* HKUnit.swift */, + F95502372EC14A32117B5F92 /* Foundation.framework */, ); - name = Common; - path = Common; + name = iOS; sourceTree = ""; }; - 473EA52459AC5B5F384D87B9 /* iOS */ = { + 82F360FB55C90EC211EF8CA1 /* Common */ = { isa = PBXGroup; children = ( - F64DEB4B031D1C5CFE809317 /* Foundation.framework */, + C199CCC759EB31B7459FAF02 /* HKUnit.swift */, + 54CADE8EBD6A8DBD34E5F08E /* LocalizedString.swift */, + 3695593AE5C331A21B37C828 /* Locked.swift */, + 3B2202DFC274BBEC075ADF5E /* OSLog.swift */, + 6ED9872057F9AB73CCE989CF /* TimeInterval.swift */, + E067B8007DAFB29BE2F03F48 /* HKUnit.swift */, ); - name = iOS; + name = Common; + path = Common; sourceTree = ""; }; - 96B57708CA5A86D6435C7B27 /* Frameworks */ = { + B59CE774003B80A203AAC25E /* G6SensorKitUI */ = { isa = PBXGroup; children = ( - F8F0591ED59D9FDBBE9B979F /* LoopKit.framework */, - 73742E0944319E3A7E474AB9 /* LoopKitUI.framework */, - 473EA52459AC5B5F384D87B9 /* iOS */, + 3C2C05844010D28B27D4E02D /* G6CGMManager+UI.swift */, + 60451EC9CB88AAED72C7B4C0 /* G6UICoordinator.swift */, + 4847E689C846CC527559717B /* LocalizedString.swift */, + D20A6B1AD620BB43E52CA138 /* G6ActivityViewController.swift */, + 6D5C11477C372E423D1838A5 /* G6ApplySensorViews.swift */, + 14F99764DE6442A33728920B /* G6CalibrationGuideView.swift */, + ABB63314B843491AD67FAFB8 /* G6DetailViews.swift */, + BC82866E887A9C466610864B /* G6Graphics.swift */, + 6BCCCBFAEC5AC32DCB17C0E0 /* G6OnboardingViews.swift */, + 7F8E7A171900BF409A482908 /* G6PackageScannerView.swift */, + E9773F6BB69F46104A715B09 /* G6ReadingDetailView.swift */, + DB76778EA2C3FE465AE8FC8F /* G6SettingsView.swift */, + 8C69491E1F3F7F01BACF348A /* Localizable.xcstrings */, + DAF739DBBA35C93627B2A12C /* Assets.xcassets */, ); - name = Frameworks; + name = G6SensorKitUI; + path = G6SensorKitUI; sourceTree = ""; }; - BC0A237992092A7C2BE9B0C1 /* G6SensorKit */ = { + BF936AEBB1ADE9BD907B2665 /* G6SensorKit */ = { isa = PBXGroup; children = ( - 03906F90369E7A2BF4DED672 /* G6Alert.swift */, - B58664EFC566ECB3D78CCFAF /* G6AlertEvaluator.swift */, - 6ED4B45A9BCAFD7FD14DE50A /* G6CGMManager.swift */, - ADFDEDB952EC717B78C87578 /* G6CGMManagerState.swift */, - 8C22FE5054AF7D52948DDD28 /* G6ShareCredentialStore.swift */, - 74783BF907EE99207B30B578 /* G6SimulatedSession.swift */, - BE9888BF3A30CE450E970741 /* Localizable.xcstrings */, + AAFC51720925E26FFF9526D8 /* G6Alert.swift */, + 5BFF9D322E96EE881F0910FD /* G6AlertEvaluator.swift */, + 2AB56C95B7CD81513D98B212 /* G6CGMManager.swift */, + 650CB121B3A64293DDF66491 /* G6CGMManagerState.swift */, + B9DBE6B77523AE6CF47FE179 /* G6ShareCredentialStore.swift */, + AFB4FF079E0E059B0A2514C4 /* G6SimulatedSession.swift */, + 2803D463F94F749B9B4C4480 /* Localizable.xcstrings */, ); name = G6SensorKit; path = G6SensorKit; sourceTree = ""; }; - C15E860FAD47DBE3D41E4EC3 /* G6SensorKitPlugin */ = { + C8A84B9B66E84F3EA74CEF1D /* Frameworks */ = { isa = PBXGroup; children = ( - 7985708466B1C12CA524C462 /* G6SensorKitPlugin.swift */, + 1B0799EB2EDF3A7864F7C5C2 /* LoopKit.framework */, + 93F66577CA895DC0539B94DB /* LoopKitUI.framework */, + 7CAD88E09B3DB8C98B60678D /* iOS */, ); - name = G6SensorKitPlugin; - path = G6SensorKitPlugin; + name = Frameworks; sourceTree = ""; }; - CC50F2BDBEBFB7D6BD059422 /* G6SensorKitUI */ = { + CC1502118BD5CBA5F06AC98B /* G6SensorKitPlugin */ = { isa = PBXGroup; children = ( - C3CDBC2872D089CCFC608914 /* G6CGMManager+UI.swift */, - 1DB91998500832B7983C5050 /* G6UICoordinator.swift */, - 5FCBBEB66782BF39C6C015CB /* LocalizedString.swift */, - 2C5694A019EAD8454DABAA13 /* G6ActivityViewController.swift */, - 76466FB9DD611978BC655632 /* G6ApplySensorViews.swift */, - CFE94E843AB267DF79DBE2E3 /* G6CalibrationGuideView.swift */, - 5F3FBE9FF2D0785F9F703D83 /* G6DetailViews.swift */, - FD441CA0C87A3361D4ABC996 /* G6Graphics.swift */, - A9A74B38BAA7E148D962602A /* G6OnboardingViews.swift */, - 0BC5A15D4A759F997F7FC541 /* G6ReadingDetailView.swift */, - 4F3226DB4412DBC72DFFA90E /* G6SettingsView.swift */, - 429E660585726604E67AECFD /* Localizable.xcstrings */, - FF491D549C3EF35172BD575E /* Assets.xcassets */, + A15C3F478B37D28F487EA970 /* G6SensorKitPlugin.swift */, ); - name = G6SensorKitUI; - path = G6SensorKitUI; + name = G6SensorKitPlugin; + path = G6SensorKitPlugin; sourceTree = ""; }; - FA22E5F750CC96151439CB10 = { + D96C91ED9D8FC6BFCEDD36AC = { isa = PBXGroup; children = ( - BC0A237992092A7C2BE9B0C1 /* G6SensorKit */, - 1DCBFFA30E3C8A0058AA3F9E /* Common */, - CC50F2BDBEBFB7D6BD059422 /* G6SensorKitUI */, - C15E860FAD47DBE3D41E4EC3 /* G6SensorKitPlugin */, - 0B2EBDB688CC9F72D51B8750 /* Products */, - 96B57708CA5A86D6435C7B27 /* Frameworks */, + BF936AEBB1ADE9BD907B2665 /* G6SensorKit */, + 82F360FB55C90EC211EF8CA1 /* Common */, + B59CE774003B80A203AAC25E /* G6SensorKitUI */, + CC1502118BD5CBA5F06AC98B /* G6SensorKitPlugin */, + 35E638A87A48A6CBC246BF6B /* Products */, + C8A84B9B66E84F3EA74CEF1D /* Frameworks */, ); sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 46CF0A3AEA3068EC92793F50 /* Headers */ = { + 07EAC52738A519CDC1D4C8D4 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 8871D9EC41336D32DC93F0A1 /* Headers */ = { + 7ADBA84EA66B2BBAFD4A6288 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - D0DCBEE1011D380E0DAB9521 /* Headers */ = { + FF4AC7DA0EA20F2B20BE3902 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( @@ -286,14 +289,14 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 1B26058BD31E40741B0058A5 /* G6SensorKit */ = { + 215CD1147465A2E980A94D63 /* G6SensorKit */ = { isa = PBXNativeTarget; - buildConfigurationList = 3C02D248834BA3228A2A0985 /* Build configuration list for PBXNativeTarget "G6SensorKit" */; + buildConfigurationList = DD280BDB51D34244226439F4 /* Build configuration list for PBXNativeTarget "G6SensorKit" */; buildPhases = ( - D0DCBEE1011D380E0DAB9521 /* Headers */, - 8BC23E60728EBBC538BC019C /* Sources */, - 4BBD731E15A4EBB02214C814 /* Frameworks */, - 840346A90686729F6FA69249 /* Resources */, + 7ADBA84EA66B2BBAFD4A6288 /* Headers */, + CF5459A40A7FD204384A3827 /* Sources */, + 7892D299DD1C4456611A5D26 /* Frameworks */, + BBB2CD01CD25C8772A7D6484 /* Resources */, ); buildRules = ( ); @@ -301,62 +304,62 @@ ); name = G6SensorKit; packageProductDependencies = ( - 7B648B7A7143DAA0CE2F4FF9 /* G6SensorCore */, + 0EC90A7F507FA56DA0FAAC1D /* G6SensorCore */, ); productName = G6SensorKit; - productReference = F7B63810D31CCFC9F7F186D2 /* G6SensorKit.framework */; + productReference = 8B421AE8197339D306979939 /* G6SensorKit.framework */; productType = "com.apple.product-type.framework"; }; - 3ADDB0CD055A33C01C7B54FE /* G6SensorKitUI */ = { + 8F3461A960A54D2320B21FE4 /* G6SensorKitUI */ = { isa = PBXNativeTarget; - buildConfigurationList = 0871B09F7297CFA1E6F3ADCB /* Build configuration list for PBXNativeTarget "G6SensorKitUI" */; + buildConfigurationList = 072C8C59C81653E06FA00EB4 /* Build configuration list for PBXNativeTarget "G6SensorKitUI" */; buildPhases = ( - 8871D9EC41336D32DC93F0A1 /* Headers */, - 488FB4793D06FDDE73E64813 /* Sources */, - 7034F2C68438154861EB8766 /* Frameworks */, - FF4CE3D3A177620D8B97B7A2 /* Resources */, + 07EAC52738A519CDC1D4C8D4 /* Headers */, + 30F8F65AC6C7D52F848495B9 /* Sources */, + CC4E9DF835B45B2D2994E5A0 /* Frameworks */, + 6D20D7C6745342F8C65404B0 /* Resources */, ); buildRules = ( ); dependencies = ( - 17B55B286FE0A660013A668D /* PBXTargetDependency */, + 2B67866B45ABC4CCDF08A03B /* PBXTargetDependency */, ); name = G6SensorKitUI; productName = G6SensorKitUI; - productReference = 0E1FE9C6D076DA1E91289E83 /* G6SensorKitUI.framework */; + productReference = 9D531F0A53057A0235E7C192 /* G6SensorKitUI.framework */; productType = "com.apple.product-type.framework"; }; - EDCCBEADE99E54A732DD551F /* G6SensorKitPlugin */ = { + D09D95523AA824744122C1CB /* G6SensorKitPlugin */ = { isa = PBXNativeTarget; - buildConfigurationList = 5C9202DE59969D7F24D99477 /* Build configuration list for PBXNativeTarget "G6SensorKitPlugin" */; + buildConfigurationList = FCDA3AF29E0740002157A96C /* Build configuration list for PBXNativeTarget "G6SensorKitPlugin" */; buildPhases = ( - 46CF0A3AEA3068EC92793F50 /* Headers */, - A4FF0114F6295250522E88B0 /* Sources */, - 08CFF0A2C0AF7A01359571D3 /* Frameworks */, - BD1DA6A29C055EAE3F534D12 /* Resources */, - 4FFBC74F2EB40DC4B5ED7514 /* Embed Frameworks */, + FF4AC7DA0EA20F2B20BE3902 /* Headers */, + D91BC325094ACAC7D108B6A0 /* Sources */, + 29459DDF79C2F6E84E026198 /* Frameworks */, + BE615E0FBD1840B4E6C34D02 /* Resources */, + D0449C47C05DB7A8F1FC65C4 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( - 07F9FA8A5643278CD0C1E3DC /* PBXTargetDependency */, - 53F05EC0188A4EBC5A8F5A4F /* PBXTargetDependency */, + 627FBE52365C3C03931C7886 /* PBXTargetDependency */, + F6FFF5B09BC08B51699968C7 /* PBXTargetDependency */, ); name = G6SensorKitPlugin; productName = G6SensorKitPlugin; - productReference = F566E5E3758DE9D2DBE17607 /* G6SensorKitPlugin.framework */; + productReference = A1A4A5B6A02A6007C52230EE /* G6SensorKitPlugin.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 9254155A3D36B6309735ED9B /* Project object */ = { + 05E8929C0E445F2264A8A045 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1600; }; - buildConfigurationList = 1F831DF837A28B0445B1871F /* Build configuration list for PBXProject "G6SensorKit" */; + buildConfigurationList = 8B609048B49027655DD83E61 /* Build configuration list for PBXProject "G6SensorKit" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; @@ -364,121 +367,122 @@ en, Base, ); - mainGroup = FA22E5F750CC96151439CB10; + mainGroup = D96C91ED9D8FC6BFCEDD36AC; minimizedProjectReferenceProxies = 0; packageReferences = ( - 17A8E591A82F95E6476AA1C9 /* XCLocalSwiftPackageReference "." */, + 222FDCD3CB726F70DF5E5FEA /* XCLocalSwiftPackageReference "." */, ); - preferredProjectObjectVersion = 77; - productRefGroup = 0B2EBDB688CC9F72D51B8750 /* Products */; + preferredProjectObjectVersion = 100; + productRefGroup = 35E638A87A48A6CBC246BF6B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 1B26058BD31E40741B0058A5 /* G6SensorKit */, - 3ADDB0CD055A33C01C7B54FE /* G6SensorKitUI */, - EDCCBEADE99E54A732DD551F /* G6SensorKitPlugin */, + 215CD1147465A2E980A94D63 /* G6SensorKit */, + 8F3461A960A54D2320B21FE4 /* G6SensorKitUI */, + D09D95523AA824744122C1CB /* G6SensorKitPlugin */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 840346A90686729F6FA69249 /* Resources */ = { + 6D20D7C6745342F8C65404B0 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3B345FE82B0B128320B404C0 /* Localizable.xcstrings in Resources */, + 2921FB176CBA1E3FACF8C998 /* Localizable.xcstrings in Resources */, + 226AE1C008E3432034691E58 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - BD1DA6A29C055EAE3F534D12 /* Resources */ = { + BBB2CD01CD25C8772A7D6484 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2C77CCC0A4FC873D451B1D9C /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - FF4CE3D3A177620D8B97B7A2 /* Resources */ = { + BE615E0FBD1840B4E6C34D02 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 04A2B67E2547E628203F1358 /* Localizable.xcstrings in Resources */, - CDBF362147EF86005600539C /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 488FB4793D06FDDE73E64813 /* Sources */ = { + 30F8F65AC6C7D52F848495B9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 04B8053FDEAE43229A81457E /* G6CGMManager+UI.swift in Sources */, - B920AFAF7C31F1E389D677D6 /* G6UICoordinator.swift in Sources */, - B379B66CE81079E346AC7C74 /* LocalizedString.swift in Sources */, - B62C7DB071276B15C354E424 /* G6ActivityViewController.swift in Sources */, - A8E3935009CCE36B2536A64C /* G6ApplySensorViews.swift in Sources */, - A9E3573682AB3B40EC374CA7 /* G6CalibrationGuideView.swift in Sources */, - 348D78E88E7AB61A3EA8B5A6 /* G6DetailViews.swift in Sources */, - C2A26CF0F1BB14468B041B94 /* G6Graphics.swift in Sources */, - 1E4F189DD2133923C1AFE92E /* G6OnboardingViews.swift in Sources */, - 43CCBACB96EC5B4551CE5E7A /* G6ReadingDetailView.swift in Sources */, - 2B177BC4AA7B5AC20A0C1CF0 /* G6SettingsView.swift in Sources */, - 5C0BDAD9F8F46B1894220D75 /* HKUnit.swift in Sources */, + 39603B3649182CA37CE8237A /* G6CGMManager+UI.swift in Sources */, + 77CD64A55933B10835781892 /* G6UICoordinator.swift in Sources */, + 405A216F8D1752F1421D4887 /* LocalizedString.swift in Sources */, + 6C6A5725B781F8EC0283A2E3 /* G6ActivityViewController.swift in Sources */, + 85D8C77F66A1FF1AF57A8F17 /* G6ApplySensorViews.swift in Sources */, + 934EFF804E6452CD19F40C04 /* G6CalibrationGuideView.swift in Sources */, + D48D98C13312034AB8C11BA8 /* G6DetailViews.swift in Sources */, + 3A7D53148400D95D5EBE9B20 /* G6Graphics.swift in Sources */, + DA3B54D9184C892B5F9A7B96 /* G6OnboardingViews.swift in Sources */, + E5928066DDD984FCE765E888 /* G6PackageScannerView.swift in Sources */, + F3BF96B43DC44EAB997C9556 /* G6ReadingDetailView.swift in Sources */, + 9D23800E9E278C62602AAF06 /* G6SettingsView.swift in Sources */, + 8587D2CC46C9C9CB94B2D3FA /* HKUnit.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 8BC23E60728EBBC538BC019C /* Sources */ = { + CF5459A40A7FD204384A3827 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8FF638A85A7E67CA3BC5D9F9 /* G6Alert.swift in Sources */, - AF6D1CDCC2147D486CC666B0 /* G6AlertEvaluator.swift in Sources */, - 44180D8F790DF0B7AD6858A6 /* G6CGMManager.swift in Sources */, - EB7246F1D6D512CC2D28B53C /* G6CGMManagerState.swift in Sources */, - C402E99C66F01260013FBB1A /* G6ShareCredentialStore.swift in Sources */, - 8BB678C1F2BD1D88FA1CFBBC /* G6SimulatedSession.swift in Sources */, - B974B8BBB65603583E2AA9B6 /* HKUnit.swift in Sources */, - 20ADEEB69E1914A498A8288C /* LocalizedString.swift in Sources */, - F63FF74AB43CFAC6FA5BB9F1 /* Locked.swift in Sources */, - 3D9CC651E9C531C097620F89 /* OSLog.swift in Sources */, - FCEF70D7853D669835A2C2A2 /* TimeInterval.swift in Sources */, + 2A9D142A81AE191360B058B0 /* G6Alert.swift in Sources */, + 9068DC2E9303B2620EC57AF2 /* G6AlertEvaluator.swift in Sources */, + 5623B9849DDAC97251E7B413 /* G6CGMManager.swift in Sources */, + 3A518597A56D377383B5D384 /* G6CGMManagerState.swift in Sources */, + 83FDE18A177B5C10B0504485 /* G6ShareCredentialStore.swift in Sources */, + 4159FE6F9D1C67AA7D355705 /* G6SimulatedSession.swift in Sources */, + 3D85A309CDAA70C948DB7D06 /* HKUnit.swift in Sources */, + 25B13BDAFD609EC61E01ED42 /* LocalizedString.swift in Sources */, + 1BFC8B0012423024BCEE84C4 /* Locked.swift in Sources */, + EE9148D1635A031059CB0474 /* OSLog.swift in Sources */, + ABC18A66F55CAC750FAAA781 /* TimeInterval.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - A4FF0114F6295250522E88B0 /* Sources */ = { + D91BC325094ACAC7D108B6A0 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F605B5DD6F6B98C7BA18BE26 /* G6SensorKitPlugin.swift in Sources */, + 81CC3E50E4370774F5954A78 /* G6SensorKitPlugin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 07F9FA8A5643278CD0C1E3DC /* PBXTargetDependency */ = { + 2B67866B45ABC4CCDF08A03B /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = G6SensorKit; - target = 1B26058BD31E40741B0058A5 /* G6SensorKit */; - targetProxy = 5DCC41D3B8AE0AF30CAAD2E3 /* PBXContainerItemProxy */; + target = 215CD1147465A2E980A94D63 /* G6SensorKit */; + targetProxy = 8C5FBFBE5966E33ED892663C /* PBXContainerItemProxy */; }; - 17B55B286FE0A660013A668D /* PBXTargetDependency */ = { + 627FBE52365C3C03931C7886 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = G6SensorKit; - target = 1B26058BD31E40741B0058A5 /* G6SensorKit */; - targetProxy = CC0AE83086E9B970A7244CCE /* PBXContainerItemProxy */; + target = 215CD1147465A2E980A94D63 /* G6SensorKit */; + targetProxy = AB91E4CF703C2153542F72B0 /* PBXContainerItemProxy */; }; - 53F05EC0188A4EBC5A8F5A4F /* PBXTargetDependency */ = { + F6FFF5B09BC08B51699968C7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = G6SensorKitUI; - target = 3ADDB0CD055A33C01C7B54FE /* G6SensorKitUI */; - targetProxy = 393AD5FC1C4D642198DBF3BA /* PBXContainerItemProxy */; + target = 8F3461A960A54D2320B21FE4 /* G6SensorKitUI */; + targetProxy = 1CA7CA75216CC94C77001C8A /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 000A188CB4AC3C534F7DEDA5 /* Debug */ = { + 3A87E12D403C48B6C4E16BCA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -498,7 +502,7 @@ GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = G6SensorKitPlugin/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKitPlugin; @@ -508,13 +512,14 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.9; TARGETED_DEVICE_FAMILY = 1; + VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WRAPPER_EXTENSION = loopplugin; }; - name = Debug; + name = Release; }; - 0A0532B628C24522B116C2FD /* Release */ = { + 3B66BBA892E68EDF726B4480 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -548,29 +553,35 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; + DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MTL_ENABLE_DEBUG_INFO = NO; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.9; - VALIDATE_PRODUCT = YES; }; - name = Release; + name = Debug; }; - 1AE1D80C0367AD875A928588 /* Release */ = { + 6965477C499968CA83551725 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -587,87 +598,24 @@ "$(inherited)", "$(BUILT_PRODUCTS_DIR)", ); - INFOPLIST_FILE = G6SensorKitUI/Info.plist; + INFOPLIST_FILE = G6SensorKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKitUI; + PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKit; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.9; TARGETED_DEVICE_FAMILY = 1; - VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Release; - }; - 222C333070D0F8651A5DDC09 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.9; - }; name = Debug; }; - 98F6F8109B911D276CB9A52E /* Debug */ = { + 6EC800F9252B079EBE43E8D0 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -684,24 +632,25 @@ "$(inherited)", "$(BUILT_PRODUCTS_DIR)", ); - INFOPLIST_FILE = G6SensorKit/Info.plist; + INFOPLIST_FILE = G6SensorKitUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKit; + PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKitUI; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.9; TARGETED_DEVICE_FAMILY = 1; + VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Debug; + name = Release; }; - D9853CD9CF2489E939E9B1B7 /* Debug */ = { + 9342504C39FA6A5C36B3F153 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -718,24 +667,25 @@ "$(inherited)", "$(BUILT_PRODUCTS_DIR)", ); - INFOPLIST_FILE = G6SensorKitUI/Info.plist; + INFOPLIST_FILE = G6SensorKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKitUI; + PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKit; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.9; TARGETED_DEVICE_FAMILY = 1; + VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Debug; + name = Release; }; - F5F1150AA306D577B24CACB2 /* Release */ = { + C2EF29AC766E13A496E2D70A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -743,7 +693,7 @@ CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = NO; CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; + DEFINES_MODULE = NO; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -752,25 +702,82 @@ "$(inherited)", "$(BUILT_PRODUCTS_DIR)", ); - INFOPLIST_FILE = G6SensorKit/Info.plist; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = G6SensorKitPlugin/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKit; + PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKitPlugin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.9; TARGETED_DEVICE_FAMILY = 1; - VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WRAPPER_EXTENSION = loopplugin; + }; + name = Debug; + }; + E1A1853C1186E503AB2BC172 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.9; + VALIDATE_PRODUCT = YES; }; name = Release; }; - FC3AF832A0B14A9957D5966C /* Release */ = { + F3ADA5588B68CDB0DC02C9F6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -778,7 +785,7 @@ CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = NO; CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = NO; + DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -787,61 +794,58 @@ "$(inherited)", "$(BUILT_PRODUCTS_DIR)", ); - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = G6SensorKitPlugin/Info.plist; + INFOPLIST_FILE = G6SensorKitUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKitPlugin; + PRODUCT_BUNDLE_IDENTIFIER = org.nightscout.G6SensorKitUI; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.9; TARGETED_DEVICE_FAMILY = 1; - VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WRAPPER_EXTENSION = loopplugin; }; - name = Release; + name = Debug; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 0871B09F7297CFA1E6F3ADCB /* Build configuration list for PBXNativeTarget "G6SensorKitUI" */ = { + 072C8C59C81653E06FA00EB4 /* Build configuration list for PBXNativeTarget "G6SensorKitUI" */ = { isa = XCConfigurationList; buildConfigurations = ( - 1AE1D80C0367AD875A928588 /* Release */, - D9853CD9CF2489E939E9B1B7 /* Debug */, + 6EC800F9252B079EBE43E8D0 /* Release */, + F3ADA5588B68CDB0DC02C9F6 /* Debug */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 1F831DF837A28B0445B1871F /* Build configuration list for PBXProject "G6SensorKit" */ = { + 8B609048B49027655DD83E61 /* Build configuration list for PBXProject "G6SensorKit" */ = { isa = XCConfigurationList; buildConfigurations = ( - 222C333070D0F8651A5DDC09 /* Debug */, - 0A0532B628C24522B116C2FD /* Release */, + 3B66BBA892E68EDF726B4480 /* Debug */, + E1A1853C1186E503AB2BC172 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 3C02D248834BA3228A2A0985 /* Build configuration list for PBXNativeTarget "G6SensorKit" */ = { + DD280BDB51D34244226439F4 /* Build configuration list for PBXNativeTarget "G6SensorKit" */ = { isa = XCConfigurationList; buildConfigurations = ( - F5F1150AA306D577B24CACB2 /* Release */, - 98F6F8109B911D276CB9A52E /* Debug */, + 9342504C39FA6A5C36B3F153 /* Release */, + 6965477C499968CA83551725 /* Debug */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 5C9202DE59969D7F24D99477 /* Build configuration list for PBXNativeTarget "G6SensorKitPlugin" */ = { + FCDA3AF29E0740002157A96C /* Build configuration list for PBXNativeTarget "G6SensorKitPlugin" */ = { isa = XCConfigurationList; buildConfigurations = ( - FC3AF832A0B14A9957D5966C /* Release */, - 000A188CB4AC3C534F7DEDA5 /* Debug */, + 3A87E12D403C48B6C4E16BCA /* Release */, + C2EF29AC766E13A496E2D70A /* Debug */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -849,18 +853,18 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 17A8E591A82F95E6476AA1C9 /* XCLocalSwiftPackageReference "." */ = { + 222FDCD3CB726F70DF5E5FEA /* XCLocalSwiftPackageReference "." */ = { isa = XCLocalSwiftPackageReference; relativePath = .; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 7B648B7A7143DAA0CE2F4FF9 /* G6SensorCore */ = { + 0EC90A7F507FA56DA0FAAC1D /* G6SensorCore */ = { isa = XCSwiftPackageProductDependency; productName = G6SensorCore; }; /* End XCSwiftPackageProductDependency section */ }; - rootObject = 9254155A3D36B6309735ED9B /* Project object */; + rootObject = 05E8929C0E445F2264A8A045 /* Project object */; } diff --git a/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKit.xcscheme b/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKit.xcscheme index 6c3c107..2a070a1 100644 --- a/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKit.xcscheme +++ b/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKit.xcscheme @@ -14,7 +14,7 @@ buildForAnalyzing = "YES"> diff --git a/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitPlugin.xcscheme b/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitPlugin.xcscheme index 7fedca5..aacd34c 100644 --- a/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitPlugin.xcscheme +++ b/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitPlugin.xcscheme @@ -14,7 +14,7 @@ buildForAnalyzing = "YES"> diff --git a/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitUI.xcscheme b/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitUI.xcscheme index ad990c8..1c91f21 100644 --- a/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitUI.xcscheme +++ b/G6SensorKit.xcodeproj/xcshareddata/xcschemes/G6SensorKitUI.xcscheme @@ -14,7 +14,7 @@ buildForAnalyzing = "YES"> diff --git a/G6SensorKit/G6CGMManager.swift b/G6SensorKit/G6CGMManager.swift index 93d4516..48707f7 100644 --- a/G6SensorKit/G6CGMManager.swift +++ b/G6SensorKit/G6CGMManager.swift @@ -69,9 +69,7 @@ public final class G6CGMManager: CGMManager { public private(set) var session: TransmitterSession? /// Commands queued for the next connection cycle. - private let lockedPendingCommands: Locked<[Command]> = Locked([]) - - private var lastBatteryReadDate: Date? + private let lockedCommandQueue: Locked = Locked(CommandQueue()) /// Response code from a session start awaiting confirmation. The reply /// alone does not say whether a session began, so it is held until the @@ -134,7 +132,7 @@ public final class G6CGMManager: CGMManager { public init(state: G6CGMManagerState) { self.lockedState = Locked(state) - lockedPendingCommands.mutate { $0 = state.pendingCommands.compactMap(Command.init(rawValue:)) } + lockedCommandQueue.mutate { $0 = CommandQueue(rawValues: state.pendingCommands) } startSession() } @@ -151,7 +149,7 @@ public final class G6CGMManager: CGMManager { } self.lockedState = Locked(state) - lockedPendingCommands.mutate { $0 = state.pendingCommands.compactMap(Command.init(rawValue:)) } + lockedCommandQueue.mutate { $0 = CommandQueue(rawValues: state.pendingCommands) } startSession() } @@ -304,11 +302,8 @@ public final class G6CGMManager: CGMManager { // is persisted, a stale code-less start could reach the transmitter // ahead of the one carrying the sensor code the user actually typed. var superseded = 0 - lockedPendingCommands.mutate { commands in - let before = commands.count - commands.removeAll { $0.supersededBy(command) } - superseded = before - commands.count - commands.append(command) + lockedCommandQueue.mutate { queue in + superseded = queue.enqueue(command) } if superseded > 0 { @@ -316,10 +311,7 @@ public final class G6CGMManager: CGMManager { } mutateState { state in - state.pendingCommands.removeAll { raw in - Command(rawValue: raw).map { $0.supersededBy(command) } ?? true - } - state.pendingCommands.append(command.rawValue) + state.pendingCommands = self.lockedCommandQueue.value.rawValues // Reflect the entered code straight away. Waiting until the // transmitter answered meant the screen said "Not used" for as @@ -332,6 +324,38 @@ public final class G6CGMManager: CGMManager { // Nudge the connection in case it is idle. session?.start() + + #if targetEnvironment(simulator) + // No link will ever drain the queue in the simulator; apply the + // command to state directly, mirroring didComplete on hardware, + // so End Sensor / Start New Sensor work end-to-end there. + lockedCommandQueue.mutate { _ = $0.dequeue() } + if case .stopSensor = command, state.sensorStartDate != nil { + let sensorEnded = PersistedCgmEvent( + date: Date(), + type: .sensorEnd, + deviceIdentifier: state.transmitterID, + failureMessage: LocalizedString("Stopped by user", comment: "Reason recorded when a session ends because the user stopped it") + ) + delegate.notify { delegate in + delegate?.cgmManager(self, hasNew: [sensorEnded]) + } + } + mutateState { state in + state.pendingCommands = self.lockedCommandQueue.value.rawValues + switch command { + case .stopSensor: + state.sensorStartDate = nil + state.sensorCode = nil + case .startSensor(let date, let sensorCode): + state.sensorCode = sensorCode.carriesParameters ? sensorCode.code : nil + state.sensorStartDate = date + .addingTimeInterval(-(state.warmupPeriod - G6CGMManager.simulatedWarmupRemaining)) + case .calibrateSensor, .resetTransmitter: + break + } + } + #endif } public func setSensorLifeDays(_ days: Int) { @@ -383,7 +407,7 @@ public final class G6CGMManager: CGMManager { if connectionPhase == .connected { log.default("Old transmitter still connected; sending session stop before switching") session?.commandSource = self - lockedPendingCommands.mutate { $0 = [.stopSensor(at: Date())] } + lockedCommandQueue.mutate { $0 = CommandQueue([.stopSensor(at: Date())]) } } else { log.default("Old transmitter not connected; its session is left as-is") } @@ -407,7 +431,7 @@ public final class G6CGMManager: CGMManager { // middle of the state reset. The session itself is kept and pointed at // the new transmitter — the radio does not need rebuilding for a swap. session?.stop() - lockedPendingCommands.mutate { $0.removeAll() } + lockedCommandQueue.mutate { $0.removeAll() } raisedAlerts = [] mutateState { state in @@ -530,9 +554,9 @@ public final class G6CGMManager: CGMManager { } } - if let last = lastBatteryReadDate, Date().timeIntervalSince(last) < Self.batteryReadInterval { - // Battery was read recently. - } else { + let batteryReadRecently = state.lastBatteryReadDate + .map { Date().timeIntervalSince($0) < Self.batteryReadInterval } ?? false + if !batteryReadRecently { session?.shouldReadBattery = true } @@ -632,7 +656,6 @@ extension G6CGMManager: TransmitterSessionDelegate { } public func transmitterSession(_ session: TransmitterSession, didReadBattery message: BatteryStatusRxMessage) { - lastBatteryReadDate = Date() log.default("Battery: A %d, B %d, resist %d", Int(message.voltageA), Int(message.voltageB), Int(message.resist)) mutateState { state in @@ -814,27 +837,10 @@ extension G6CGMManager: TransmitterSessionDelegate { } private func glucoseTrend(for glucose: Glucose) -> GlucoseTrend? { - guard let rate = glucose.trendRateMgDLPerMinute else { + guard let arrow = G6TrendArrow(dexcomRateMgDLPerMinute: glucose.trendRateMgDLPerMinute) else { return nil } - - // Standard Dexcom trend arrow thresholds in (mg/dL)/min. - switch rate { - case ..<(-3): - return .downDownDown - case ..<(-2): - return .downDown - case ..<(-1): - return .down - case ..<1: - return .flat - case ..<2: - return .up - case ..<3: - return .upUp - default: - return .upUpUp - } + return GlucoseTrend(rawValue: arrow.rawValue) } } @@ -846,36 +852,17 @@ extension G6CGMManager: TransmitterCommandSource { public func dequeuePendingCommand(for session: TransmitterSession) -> Command? { var next: Command? var dropped: [Command] = [] - lockedPendingCommands.mutate { commands in - // A calibration is only meaningful next to the fingerstick it came - // from; xDrip uses the same five-minute window. Sending a stale one - // would teach the sensor from a value that no longer holds. - while let candidate = commands.first { - commands.removeFirst() - if case .calibrateSensor(_, let date) = candidate, - Date().timeIntervalSince(date) > .minutes(5) { - dropped.append(candidate) - continue - } - next = candidate - break - } + lockedCommandQueue.mutate { queue in + (next, dropped) = queue.dequeue() } for command in dropped { log.default("Dropping stale command: %{public}@", String(describing: command)) - mutateState { state in - if !state.pendingCommands.isEmpty { - state.pendingCommands.removeFirst() - } - } } - if next != nil { + if next != nil || !dropped.isEmpty { mutateState { state in - if !state.pendingCommands.isEmpty { - state.pendingCommands.removeFirst() - } + state.pendingCommands = self.lockedCommandQueue.value.rawValues } } @@ -911,6 +898,17 @@ extension G6CGMManager: TransmitterCommandSource { state.lastSessionStartFailure = nil } case .stopSensor: + if state.sensorStartDate != nil { + let sensorEnded = PersistedCgmEvent( + date: Date(), + type: .sensorEnd, + deviceIdentifier: state.transmitterID, + failureMessage: LocalizedString("Stopped by user", comment: "Reason recorded when a session ends because the user stopped it") + ) + delegate.notify { delegate in + delegate?.cgmManager(self, hasNew: [sensorEnded]) + } + } mutateState { state in state.sensorStartDate = nil state.sensorCode = nil @@ -933,18 +931,10 @@ struct G6GlucoseDisplay: GlucoseDisplayable { } var trendType: GlucoseTrend? { - guard let rate = reading.trendRateMgDLPerMinute else { + guard let arrow = G6TrendArrow(dexcomRateMgDLPerMinute: reading.trendRateMgDLPerMinute) else { return nil } - switch rate { - case ..<(-3): return .downDownDown - case ..<(-2): return .downDown - case ..<(-1): return .down - case ..<1: return .flat - case ..<2: return .up - case ..<3: return .upUp - default: return .upUpUp - } + return GlucoseTrend(rawValue: arrow.rawValue) } var trendRate: HKQuantity? { diff --git a/G6SensorKit/G6CGMManagerState.swift b/G6SensorKit/G6CGMManagerState.swift index 40b6039..d96eb22 100644 --- a/G6SensorKit/G6CGMManagerState.swift +++ b/G6SensorKit/G6CGMManagerState.swift @@ -308,7 +308,14 @@ public struct G6CGMManagerState: RawRepresentable { return sensorStartDate?.addingTimeInterval(sensorLife) } + /// Unknown until the first version read: the fallback constants in + /// `transmitterLifetime` are guesses (a 180-day Anubis would alert as + /// "expired" against the stock 90 days), so no expiry is reported before + /// the transmitter's own value is in. public var transmitterExpirationDate: Date? { + guard transmitterExpiryInDays != nil else { + return nil + } return transmitterStartDate?.addingTimeInterval(transmitterLifetime) } @@ -338,11 +345,9 @@ public struct G6CGMManagerState: RawRepresentable { /// The device model implied by the transmitter ID prefix. G6 transmitters /// use `8…`; Dexcom ONE uses `5…` or `C…`. public var deviceModel: String { - switch transmitterID.first { - case "5", "C", "c": - return "Dexcom ONE" - default: - return "Dexcom G6" + switch G6TransmitterModel(transmitterID: transmitterID) { + case .g6: return "Dexcom G6" + case .one: return "Dexcom ONE" } } } diff --git a/G6SensorKit/G6SimulatedSession.swift b/G6SensorKit/G6SimulatedSession.swift index cc886c3..30c4aac 100644 --- a/G6SensorKit/G6SimulatedSession.swift +++ b/G6SensorKit/G6SimulatedSession.swift @@ -43,7 +43,9 @@ extension G6CGMManager { // warming up. state.transmitterStartDate = now.addingTimeInterval(-.hours(24 * 12)) state.sensorStartDate = now.addingTimeInterval(-(state.warmupPeriod - Self.simulatedWarmupRemaining)) - state.transmitterExpiryInDays = 90 + // 180 days so the simulator presents as an Anubis, making the + // longer session length and expiry visible in the UI. + state.transmitterExpiryInDays = 180 state.firmwareVersion = "1.0.0.0 (simulated)" state.peripheralIdentifier = UUID() } @@ -64,6 +66,9 @@ extension G6CGMManager { } private func emitSimulatedReading() { + // No active session (ended, or not yet started): stay quiet. + guard state.sensorStartDate != nil else { return } + guard !state.isInWarmup else { // Still warming up: the state change alone refreshes the UI. notifySimulationObservers() diff --git a/G6SensorKit/Localizable.xcstrings b/G6SensorKit/Localizable.xcstrings index ff7977d..8d6f1b6 100644 --- a/G6SensorKit/Localizable.xcstrings +++ b/G6SensorKit/Localizable.xcstrings @@ -120,6 +120,10 @@ "mg/dL": { "extractionState": "manual", "comment": "The short unit display string for milligrams of glucose per deciliter" + }, + "Stopped by user": { + "extractionState": "manual", + "comment": "Reason recorded when a session ends because the user stopped it" } }, "version": "1.0" diff --git a/G6SensorKitUI/Localizable.xcstrings b/G6SensorKitUI/Localizable.xcstrings index f218d5c..309d047 100644 --- a/G6SensorKitUI/Localizable.xcstrings +++ b/G6SensorKitUI/Localizable.xcstrings @@ -1,838 +1,904 @@ { "sourceLanguage": "en", "strings": { - "Sensor Failed": { - "extractionState": "manual", - "comment": "Status highlight for failed sensor" + "": { }, - "Sensor Warmup": { - "extractionState": "manual", - "comment": "Status highlight during warm-up" + "—": { + "comment": "Placeholder for missing value", + "extractionState": "manual" }, - "Transmitter Expired": { - "extractionState": "manual", - "comment": "Status highlight when the transmitter is past end of life" + "“Dexcom”, “G6”, and “ONE” are trademarks of Dexcom, Inc., used here only to describe which devices are compatible. This app is not made by or endorsed by Dexcom.": { + "comment": "Trademark attribution footer", + "extractionState": "manual" }, - "No Sensor": { - "extractionState": "manual", - "comment": "Status highlight when no session is running" + "%@ remaining": { + "comment": "Session remaining (1: duration)", + "extractionState": "manual" }, - "Sensor Expired": { - "extractionState": "manual", - "comment": "Status highlight for expired sensor" + "%@ until ready": { + "comment": "Warm-up remaining (1: duration)", + "extractionState": "manual" + }, + "%@. %@": { + "comment": "A label that describes the phase and detail of a lifecycle bar.", + "isCommentAutoGenerated": true, + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "%1$@. %2$@" + } + } + } }, - "Calibration Needed": { - "extractionState": "manual", - "comment": "Status highlight when a calibration is requested" + "%1$@ talks to your G6 or Dexcom ONE transmitter directly. It starts and stops sensor sessions, sends your calibrations, and fills in readings it missed.": { + "comment": "Introduction: what the native driver does (1: app name)", + "extractionState": "manual" }, - "Signal Loss": { - "extractionState": "manual", - "comment": "Status highlight for signal loss" + "%1$@%2$@ per minute": { + "comment": "Trend rate (1: sign, 2: formatted rate with unit)", + "extractionState": "manual" }, - "This app": { - "extractionState": "manual", - "comment": "Fallback when the host app name cannot be read" + "%1$@%2$@/min": { + "comment": "Trend rate (1: sign, 2: formatted rate with unit)", + "extractionState": "manual" }, - "Transmitter ID": { - "extractionState": "manual", - "comment": "Navigation title for transmitter ID entry" + "%d °C": { + "comment": "Temperature in Celsius (1: degrees)", + "extractionState": "manual" }, - "Sensor Code": { - "extractionState": "manual", - "comment": "Navigation title for sensor code entry" + "%d days": { + "comment": "Number of days (1: day count)", + "extractionState": "manual" }, - "Applying Your Sensor": { - "extractionState": "manual", - "comment": "Navigation title for placement guide" + "%d min ago": { + "comment": "Minutes since last reading (1: minutes)", + "extractionState": "manual" }, - "Pairing": { - "extractionState": "manual", - "comment": "Navigation title for pairing screen" + "%d mV": { + "comment": "Voltage in millivolts (1: millivolts)", + "extractionState": "manual" }, - "Warm-Up": { - "extractionState": "manual", - "comment": "Navigation title for warm-up screen" + "%dd %dh": { + "comment": "Duration in days and hours", + "extractionState": "manual" }, - "Dexcom G6 / ONE": { - "extractionState": "manual", - "comment": "Navigation title for settings" + "%dh %dm": { + "comment": "Duration in hours and minutes", + "extractionState": "manual" }, - "Calibration": { - "extractionState": "manual", - "comment": "Navigation title for calibration" + "%dm": { + "comment": "Duration in minutes", + "extractionState": "manual" }, - "Transmitter": { - "extractionState": "manual", - "comment": "Navigation title for transmitter details" + "%lld": { + "comment": "A number in the center of a circle.", + "isCommentAutoGenerated": true }, - "Session Length": { - "extractionState": "manual", - "comment": "Navigation title for sensor life settings" + "4-digit code": { + "comment": "Sensor code text field placeholder", + "extractionState": "manual" }, - "Dexcom Share": { - "extractionState": "manual", - "comment": "Navigation title for Share upload settings" + "6-character ID": { + "comment": "Transmitter ID text field placeholder", + "extractionState": "manual" }, - "INSERT SENSOR": { - "extractionState": "manual", - "comment": "Section label for the insert-sensor steps" + "A display-only value has been shifted by a recent calibration. It is shown for continuity and is not used for dosing.": { + "comment": "Footer explaining display-only readings", + "extractionState": "manual" }, - "STEP %d of 10": { - "extractionState": "manual", - "comment": "Insert-sensor step counter (1: step number)" + "A pairing request will appear": { + "comment": "Pairing callout title", + "extractionState": "manual" }, - "Thoroughly wash and dry your hands.": { - "extractionState": "manual", - "comment": "Insert step 1 body" + "Accept the Bluetooth pairing request within about a minute. If it disappears, the app tries again on the next connection.": { + "comment": "Pairing callout body while awaiting the prompt", + "extractionState": "manual" }, - "Clean the insertion site with alcohol. Let it dry.": { - "extractionState": "manual", - "comment": "Insert step 2 body" + "Active": { + "comment": "Session phase: active", + "extractionState": "manual" }, - "The site must be completely dry or the adhesive will not stick.": { - "extractionState": "manual", - "comment": "Insert step 2 note" + "Adults wear the sensor on the belly. Children aged 2 to 17 can use the belly or upper buttocks.": { + "comment": "Apply-sensor site instruction", + "extractionState": "manual" }, - "Optional: if you use a skin adhesive, draw an empty oval on the skin and let it dry.": { - "extractionState": "manual", - "comment": "Insert step 3 body" + "After warm-up": { + "comment": "Why calibration is unavailable during warm-up", + "extractionState": "manual" }, - "Insert the sensor on clean skin in the centre of the oval, not on the adhesive itself.": { - "extractionState": "manual", - "comment": "Insert step 3 note" + "Allow camera access in Settings to scan the Data Matrix code on your Dexcom transmitter packaging.": { + "comment": "Camera denied alert message", + "extractionState": "manual" }, - "Take out the applicator whose sensor code you entered, and peel off its cover.": { - "extractionState": "manual", - "comment": "Insert step 4 body" + "Apply a new sensor": { + "comment": "Apply-sensor screen title", + "extractionState": "manual" }, - "Do not use it if the packaging is damaged or was already opened. Keep the packaging until the session is finished.": { - "extractionState": "manual", - "comment": "Insert step 4 note" + "Applying Your Sensor": { + "comment": "Navigation title for placement guide", + "extractionState": "manual" }, - "Pull off both adhesive labels.": { - "extractionState": "manual", - "comment": "Insert step 5 body" + "ATTACH TRANSMITTER": { + "comment": "Section label for the attach-transmitter steps", + "extractionState": "manual" }, - "Keep the tab showing the sensor code. Do not touch the adhesive.": { - "extractionState": "manual", - "comment": "Insert step 5 note" + "Battery": { + "comment": "Transmitter details section header for battery", + "extractionState": "manual" }, - "Place the applicator flat against your skin — horizontally, not upright — and press down firmly.": { - "extractionState": "manual", - "comment": "Insert step 6 body" + "Battery B is the cell that runs down first and determines whether the transmitter can finish a session.": { + "comment": "Footer explaining what battery B means", + "extractionState": "manual" }, - "Fold and break off the safety guard, then throw it away.": { - "extractionState": "manual", - "comment": "Insert step 7 body" + "CALIBRATE": { + "comment": "Section label for the calibration steps", + "extractionState": "manual" }, - "Push and release the button to insert the sensor.": { - "extractionState": "manual", - "comment": "Insert step 8 body" + "Calibration": { + "comment": "Navigation title for calibration", + "extractionState": "manual" }, - "Lift the applicator away.": { - "extractionState": "manual", - "comment": "Insert step 9 body" + "Calibration becomes available once warm-up finishes. Do not make insulin decisions from early readings — use a fingerstick meter and follow your care team's guidance.": { + "comment": "Footer explaining warm-up restrictions", + "extractionState": "manual" }, - "Dispose of the applicator following your local rules for items that have touched blood.": { - "extractionState": "manual", - "comment": "Insert step 9 note" + "Calibration Needed": { + "comment": "Status highlight when a calibration is requested", + "extractionState": "manual" }, - "Left on your skin: the sensor wire and the transmitter holder. The sensor is in.": { - "extractionState": "manual", - "comment": "Insert step 10 body" + "Calibration requested": { + "comment": "Algorithm state: needs calibration", + "extractionState": "manual" }, - "ATTACH TRANSMITTER": { - "extractionState": "manual", - "comment": "Section label for the attach-transmitter steps" + "Calibrations must be between %1$@ and %2$@. If your meter reads outside that range, treat it and follow your care team's guidance instead of calibrating.": { + "comment": "Validation message for out-of-range calibration (1: minimum with unit, 2: maximum with unit)", + "extractionState": "manual" }, - "STEP %d of 4": { - "extractionState": "manual", - "comment": "Attach-transmitter step counter (1: step number)" + "Camera access is off": { + "comment": "Camera denied alert title", + "extractionState": "manual" }, - "Take the transmitter out of its box and prepare it for use. Wipe the bottom with an alcohol wipe and let it dry.": { - "extractionState": "manual", - "comment": "Attach step 1 body" + "Cancel": { + "comment": "Cancel button", + "extractionState": "manual" }, - "Do not touch the metal contacts and do not scratch the bottom — that can break the waterproof seal.": { - "extractionState": "manual", - "comment": "Attach step 1 note" + "Check that the Dexcom app is not connected to this transmitter, that the transmitter ID is exactly right, and that your phone is next to it. Bluetooth must be on.": { + "comment": "Pairing troubleshooting body", + "extractionState": "manual" }, - "Slide the transmitter tab into the slot at the narrow end of the holder.": { - "extractionState": "manual", - "comment": "Attach step 3 body" + "Clean the insertion site with alcohol. Let it dry.": { + "comment": "Insert step 2 body", + "extractionState": "manual" }, - "Press the wide end of the transmitter until it clicks into the holder.": { - "extractionState": "manual", - "comment": "Attach step 4 body" + "Connected to your transmitter": { + "comment": "Pairing status: connected", + "extractionState": "manual" }, - "Rub your fingers around the patch three times to secure it.": { - "extractionState": "manual", - "comment": "Attach step 5 body" + "Connecting to your transmitter": { + "comment": "Pairing screen heading", + "extractionState": "manual" }, - "You are almost done — the sensor session starts next.": { - "extractionState": "manual", - "comment": "Attach step 5 note" + "Continue": { + "comment": "Confirm pairing a new transmitter", + "extractionState": "manual" }, - "Apply a new sensor": { - "extractionState": "manual", - "comment": "Apply-sensor screen title" + "Could not save your credentials. Try again.": { + "comment": "Status message when saving Share credentials fails", + "extractionState": "manual" }, - "Adults wear the sensor on the belly. Children aged 2 to 17 can use the belly or upper buttocks.": { - "extractionState": "manual", - "comment": "Apply-sensor site instruction" + "Credentials saved to the Keychain.": { + "comment": "Status message after saving Share credentials", + "extractionState": "manual" }, - "Keep at least 8 cm (3 inches) away from insulin pump sites and injection sites, and avoid scars, bony areas, and your waistband.": { - "extractionState": "manual", - "comment": "Apply-sensor spacing instruction" + "Device": { + "comment": "Reading detail label for device", + "extractionState": "manual" }, - "HOW TO APPLY A SENSOR": { - "extractionState": "manual", - "comment": "Apply-sensor help button" + "Dexcom Account": { + "comment": "Section header for Share account", + "extractionState": "manual" }, - "Next": { - "extractionState": "manual", - "comment": "Next button" + "Dexcom G6": { + "comment": "Introduction screen product title", + "extractionState": "manual" }, - "How to apply a sensor": { - "extractionState": "manual", - "comment": "Apply-sensor help screen title" + "Dexcom G6 / ONE": { + "comment": "Navigation title for settings", + "extractionState": "manual" }, - "Done": { - "extractionState": "manual", - "comment": "Done button" + "Dexcom Share": { + "comment": "Navigation title for Share upload settings", + "extractionState": "manual" }, - "CALIBRATE": { - "extractionState": "manual", - "comment": "Section label for the calibration steps" + "Dexcom Share Upload": { + "comment": "Row to Share upload settings", + "extractionState": "manual" }, - "STEP %d of 3": { - "extractionState": "manual", - "comment": "Calibration step counter (1: step number)" + "Display only": { + "comment": "Reading detail label for display-only flag", + "extractionState": "manual" }, - "Wash and dry your hands with soap and water — not a gel cleaner.": { - "extractionState": "manual", - "comment": "Calibration step 1 body" + "Dispose of the applicator following your local rules for items that have touched blood.": { + "comment": "Insert step 9 note", + "extractionState": "manual" }, - "Poorly washed hands are behind most inaccurate meter readings, and an inaccurate reading teaches the sensor the wrong thing.": { - "extractionState": "manual", - "comment": "Calibration step 1 note" + "Do not dose on early readings": { + "comment": "Warm-up callout title", + "extractionState": "manual" }, - "Take a blood sample from your fingertip and measure it with your meter.": { - "extractionState": "manual", - "comment": "Calibration step 2 body" + "Do not make insulin decisions from the first readings after warm-up, or from readings that look wrong for how you feel. Use a fingerstick meter instead, and follow your care team's guidance.": { + "comment": "Warm-up callout body about dosing safety", + "extractionState": "manual" }, - "Use a fingertip only — samples from other sites are less accurate. Use the same meter throughout a sensor session, and check that your strips are in date.": { - "extractionState": "manual", - "comment": "Calibration step 2 note" + "Do not round the number or reuse an older reading. If more than 5 minutes have passed, measure again.": { + "comment": "Calibration step 3 note", + "extractionState": "manual" }, - "Enter the exact value your meter showed, within 5 minutes of the fingerstick.": { - "extractionState": "manual", - "comment": "Calibration step 3 body" + "Do not touch the metal contacts and do not scratch the bottom — that can break the waterproof seal.": { + "comment": "Attach step 1 note", + "extractionState": "manual" }, - "Do not round the number or reuse an older reading. If more than 5 minutes have passed, measure again.": { - "extractionState": "manual", - "comment": "Calibration step 3 note" + "Do not use it if the packaging is damaged or was already opened. Keep the packaging until the session is finished.": { + "comment": "Insert step 4 note", + "extractionState": "manual" }, - "How to calibrate": { - "extractionState": "manual", - "comment": "Calibration help screen title" + "Done": { + "comment": "Done button", + "extractionState": "manual" + }, + "Ending Soon": { + "comment": "Session phase: expiring soon", + "extractionState": "manual" }, "Enter a fingerstick value": { - "extractionState": "manual", - "comment": "Calibration screen heading" + "comment": "Calibration screen heading", + "extractionState": "manual" }, - "Use a fresh fingerstick reading from your meter, taken just now. Wash and dry your hands first — a value from unwashed fingers can be badly wrong and will teach the sensor the wrong thing.": { - "extractionState": "manual", - "comment": "Calibration instructions" + "Enter a number.": { + "comment": "Validation message for non-numeric calibration entry", + "extractionState": "manual" }, - "HOW TO CALIBRATE": { - "extractionState": "manual", - "comment": "Calibration help button" + "Enter Calibration": { + "comment": "Button to enter a calibration", + "extractionState": "manual" }, - "It may take a few minutes": { - "extractionState": "manual", - "comment": "Calibration callout title" + "Enter the Code": { + "comment": "Return to entering the sensor code", + "extractionState": "manual" }, - "The calibration is sent the next time your transmitter connects, which can be up to 5 minutes away. The transmitter may accept it, ask for a second value, or reject it — this screen's status will tell you which.": { - "extractionState": "manual", - "comment": "Calibration callout body about delivery timing" + "Enter the exact value your meter showed, within 5 minutes of the fingerstick.": { + "comment": "Calibration step 3 body", + "extractionState": "manual" }, - "Send Calibration": { - "extractionState": "manual", - "comment": "Calibration submit button" + "Enter your sensor code": { + "comment": "Sensor code screen heading", + "extractionState": "manual" }, - "Enter a number.": { - "extractionState": "manual", - "comment": "Validation message for non-numeric calibration entry" + "Enter your transmitter ID": { + "comment": "Transmitter ID screen heading", + "extractionState": "manual" }, - "Pair New Transmitter": { - "extractionState": "manual", - "comment": "Row to pair a different transmitter" + "Expected to expire": { + "comment": "Transmitter detail label for expiry", + "extractionState": "manual" }, - "Pair a new transmitter?": { - "extractionState": "manual", - "comment": "Confirmation title for pairing a new transmitter" + "Expected to finish at %1$@, about %2$d minutes from now": { + "comment": "Warm-up completion time (1: time, 2: minutes remaining)", + "extractionState": "manual" }, - "Continue": { - "extractionState": "manual", - "comment": "Confirm pairing a new transmitter" + "Firmware": { + "comment": "Transmitter detail label for firmware", + "extractionState": "manual" }, - "Your current sensor session will end. A session lives on the transmitter, so it cannot move to a new one — you will start a fresh session after pairing, and can reuse the sensor you are wearing. Stop the sensor first if your old transmitter is still attached and you want it stopped cleanly. Your other settings are kept.": { - "extractionState": "manual", - "comment": "Confirmation message for pairing a new transmitter while a session is running" + "First used": { + "comment": "Transmitter detail label for activation date", + "extractionState": "manual" }, - "You will enter the new transmitter's ID and can then start a sensor session on it. Your other settings are kept.": { - "extractionState": "manual", - "comment": "Confirmation message for pairing a new transmitter with no session running" + "Fold and break off the safety guard, then throw it away.": { + "comment": "Insert step 7 body", + "extractionState": "manual" }, - "Model": { - "extractionState": "manual", - "comment": "Transmitter detail label for model" + "How to apply a sensor": { + "comment": "Apply-sensor help screen title", + "extractionState": "manual" }, - "Firmware": { - "extractionState": "manual", - "comment": "Transmitter detail label for firmware" + "How to Apply a Sensor": { + "comment": "Row to the placement guide", + "extractionState": "manual" }, - "Unknown": { - "extractionState": "manual", - "comment": "Placeholder for unknown value" + "HOW TO APPLY A SENSOR": { + "comment": "Apply-sensor help button", + "extractionState": "manual" }, - "First used": { - "extractionState": "manual", - "comment": "Transmitter detail label for activation date" + "How to calibrate": { + "comment": "Calibration help screen title", + "extractionState": "manual" }, - "Expected to expire": { - "extractionState": "manual", - "comment": "Transmitter detail label for expiry" + "HOW TO CALIBRATE": { + "comment": "Calibration help button", + "extractionState": "manual" }, - "Reported lifetime": { - "extractionState": "manual", - "comment": "Transmitter detail label for reported lifetime" + "Identifier": { + "comment": "Reading detail label for sync identifier", + "extractionState": "manual" }, - "%d days": { - "extractionState": "manual", - "comment": "Number of days (1: day count)" + "IDs that begin with 4 belong to Dexcom G5 transmitters, which this app does not support.": { + "comment": "Validation message for G5 transmitter ID", + "extractionState": "manual" }, - "Lifetime": { - "extractionState": "manual", - "comment": "Transmitter details section header for lifetime" + "INSERT SENSOR": { + "comment": "Section label for the insert-sensor steps", + "extractionState": "manual" }, - "This transmitter is past the end of its life. It still reports readings for a running session, but cannot start a new sensor.": { - "extractionState": "manual", - "comment": "Footer shown when the transmitter has expired" + "Insert the sensor on clean skin in the centre of the oval, not on the adhesive itself.": { + "comment": "Insert step 3 note", + "extractionState": "manual" }, - "This transmitter reports an extended 180-day lifetime, which means it has been modified. Warm-up is shorter and you can choose how long sessions run.": { - "extractionState": "manual", - "comment": "Footer explaining Anubis detection" + "It may take a few minutes": { + "comment": "Calibration callout title", + "extractionState": "manual" }, - "Session length: %d days": { - "extractionState": "manual", - "comment": "Session length value (1: day count)" + "Just now": { + "comment": "Reading received less than a minute ago", + "extractionState": "manual" }, - "Modified transmitters can keep a sensor running past the standard 10 days. This setting only changes when this app treats the session as finished; it does not change the sensor's accuracy. Sensor readings can drift the longer a sensor is worn, so check against a fingerstick meter if a reading does not match how you feel.": { - "extractionState": "manual", - "comment": "Footer explaining extended session length and its accuracy caveat" + "Keep at least 8 cm (3 inches) away from insulin pump sites and injection sites, and avoid scars, bony areas, and your waistband.": { + "comment": "Apply-sensor spacing instruction", + "extractionState": "manual" }, - "Upload to Dexcom Share": { - "extractionState": "manual", - "comment": "Toggle label for Share upload" + "Keep the tab showing the sensor code. Do not touch the adhesive.": { + "comment": "Insert step 5 note", + "extractionState": "manual" }, - "Turning this on sends your readings to Dexcom Share so people who follow you can see them in the Dexcom Follow app. It is optional, and your readings still work in this app without it.": { - "extractionState": "manual", - "comment": "Footer explaining Share upload" + "Last checked": { + "comment": "Transmitter detail label for last battery read", + "extractionState": "manual" }, - "Username": { - "extractionState": "manual", - "comment": "Share account username field" + "Last Reading": { + "comment": "Settings section: last reading", + "extractionState": "manual" }, - "Password": { - "extractionState": "manual", - "comment": "Share account password field" + "Latest Reading": { + "comment": "Reading detail screen title", + "extractionState": "manual" }, - "Save Credentials": { - "extractionState": "manual", - "comment": "Button to save Share credentials" + "Left on your skin: the sensor wire and the transmitter holder. The sensor is in.": { + "comment": "Insert step 10 body", + "extractionState": "manual" }, - "Dexcom Account": { - "extractionState": "manual", - "comment": "Section header for Share account" + "Lifetime": { + "comment": "Transmitter details section header for lifetime", + "extractionState": "manual" }, - "Your username and password are stored in the iOS Keychain on this device only. They are never written into app settings, logs, or backups of this app's data.": { - "extractionState": "manual", - "comment": "Footer explaining Keychain credential storage" + "Lift the applicator away.": { + "comment": "Insert step 9 body", + "extractionState": "manual" }, - "Upload turned off and saved credentials removed.": { - "extractionState": "manual", - "comment": "Status message after disabling Share upload" + "Looking for your transmitter…": { + "comment": "Pairing status: searching", + "extractionState": "manual" }, - "Credentials saved to the Keychain.": { - "extractionState": "manual", - "comment": "Status message after saving Share credentials" + "Model": { + "comment": "Transmitter detail label for model", + "extractionState": "manual" }, - "Could not save your credentials. Try again.": { - "extractionState": "manual", - "comment": "Status message when saving Share credentials fails" + "Modified transmitters can keep a sensor running past the standard 10 days. This setting only changes when this app treats the session as finished; it does not change the sensor's accuracy. Sensor readings can drift the longer a sensor is worn, so check against a fingerstick meter if a reading does not match how you feel.": { + "comment": "Footer explaining extended session length and its accuracy caveat", + "extractionState": "manual" }, - "Warming Up": { - "extractionState": "manual", - "comment": "Session phase: warm-up" + "Next": { + "comment": "Next button", + "extractionState": "manual" }, - "Active": { - "extractionState": "manual", - "comment": "Session phase: active" + "No": { + "comment": "Negative", + "extractionState": "manual" }, - "Ending Soon": { - "extractionState": "manual", - "comment": "Session phase: expiring soon" + "No code on your sensor?": { + "comment": "Sensor code callout title", + "extractionState": "manual" }, - "Session Ended": { - "extractionState": "manual", - "comment": "Session phase: expired" + "No readings — replace the sensor.": { + "comment": "Placeholder when the sensor has failed", + "extractionState": "manual" }, - "No Signal": { - "extractionState": "manual", - "comment": "Session phase: signal loss" + "No sensor": { + "comment": "Why calibration is unavailable without a session", + "extractionState": "manual" }, - "Dexcom G6": { - "extractionState": "manual", - "comment": "Introduction screen product title" + "No Sensor": { + "comment": "Status highlight when no session is running", + "extractionState": "manual" }, - "%1$@ talks to your G6 or Dexcom ONE transmitter directly. It starts and stops sensor sessions, sends your calibrations, and fills in readings it missed.": { - "extractionState": "manual", - "comment": "Introduction: what the native driver does (1: app name)" + "No Signal": { + "comment": "Session phase: signal loss", + "extractionState": "manual" }, - "You do not need the Dexcom app — and you must not let it use the same transmitter at the same time.": { - "extractionState": "manual", - "comment": "Introduction: single-app requirement" + "Not available": { + "comment": "Value when the transmitter reports no trend", + "extractionState": "manual" }, - "Enter your transmitter ID": { - "extractionState": "manual", - "comment": "Transmitter ID screen heading" + "Not used": { + "comment": "Value when a session runs without a sensor code", + "extractionState": "manual" + }, + "OK": { + "comment": "Algorithm state: ok", + "extractionState": "manual" }, "On the transmitter box, next to the barcode.": { - "extractionState": "manual", - "comment": "Transmitter ID: location on the box" + "comment": "Transmitter ID: location on the box", + "extractionState": "manual" + }, + "Open Settings": { + "comment": "Open Settings button", + "extractionState": "manual" + }, + "Optional: if you use a skin adhesive, draw an empty oval on the skin and let it dry.": { + "comment": "Insert step 3 body", + "extractionState": "manual" }, "Or on the back of the transmitter itself.": { - "extractionState": "manual", - "comment": "Transmitter ID: location on the transmitter" + "comment": "Transmitter ID: location on the transmitter", + "extractionState": "manual" }, - "6-character ID": { - "extractionState": "manual", - "comment": "Transmitter ID text field placeholder" + "Pair a new transmitter?": { + "comment": "Confirmation title for pairing a new transmitter", + "extractionState": "manual" }, - "This app works with Dexcom G6 and Dexcom ONE transmitters. Older G5 transmitters, whose IDs begin with 4, are not supported.": { - "extractionState": "manual", - "comment": "Transmitter ID: supported hardware note" + "Pair New Transmitter": { + "comment": "Row to pair a different transmitter", + "extractionState": "manual" }, - "IDs that begin with 4 belong to Dexcom G5 transmitters, which this app does not support.": { - "extractionState": "manual", - "comment": "Validation message for G5 transmitter ID" + "Pairing": { + "comment": "Navigation title for pairing screen", + "extractionState": "manual" }, - "Enter your sensor code": { - "extractionState": "manual", - "comment": "Sensor code screen heading" + "Password": { + "comment": "Share account password field", + "extractionState": "manual" }, - "The 4-digit code is on the applicator’s adhesive label, and is unique to that sensor. Use the code from the applicator you are about to insert — a code from a different one will make readings inaccurate.": { - "extractionState": "manual", - "comment": "Sensor code: where to find it" + "Place the applicator flat against your skin — horizontally, not upright — and press down firmly.": { + "comment": "Insert step 6 body", + "extractionState": "manual" }, - "4-digit code": { - "extractionState": "manual", - "comment": "Sensor code text field placeholder" + "Poorly washed hands are behind most inaccurate meter readings, and an inaccurate reading teaches the sensor the wrong thing.": { + "comment": "Calibration step 1 note", + "extractionState": "manual" }, - "No code on your sensor?": { - "extractionState": "manual", - "comment": "Sensor code callout title" + "Press the wide end of the transmitter until it clicks into the holder.": { + "comment": "Attach step 4 body", + "extractionState": "manual" }, - "You can start without one. The transmitter will then ask you for two fingerstick glucose values after warm-up, and for one more each day. \"Calibration\" means telling the sensor what your meter reads, so it can correct itself.": { - "extractionState": "manual", - "comment": "Sensor code callout body explaining calibration" + "Pull off both adhesive labels.": { + "comment": "Insert step 5 body", + "extractionState": "manual" }, - "Use This Code": { - "extractionState": "manual", - "comment": "Sensor code submit button" + "Push and release the button to insert the sensor.": { + "comment": "Insert step 8 body", + "extractionState": "manual" }, - "Start Without a Code": { - "extractionState": "manual", - "comment": "Button to start a session with no sensor code" + "Reading": { + "comment": "Reading detail section header", + "extractionState": "manual" }, - "Start without a sensor code?": { - "extractionState": "manual", - "comment": "Confirmation title for a code-less session" + "Readings will appear as they arrive from the transmitter, about every 5 minutes.": { + "comment": "Explanation shown once warm-up has completed", + "extractionState": "manual" }, - "Enter the Code": { - "extractionState": "manual", - "comment": "Return to entering the sensor code" + "Remove CGM": { + "comment": "Confirm CGM removal", + "extractionState": "manual" }, - "Without the code the sensor cannot use its factory calibration, and the transmitter will ask you for two fingerstick values after warm-up and one every day after that. The code is on the applicator label.": { - "extractionState": "manual", - "comment": "Confirmation message explaining a code-less session" + "Remove CGM from App": { + "comment": "Button to delete the CGM manager", + "extractionState": "manual" }, - "That code isn't one this app recognizes. Check the digits on the applicator label, or start without a code.": { - "extractionState": "manual", - "comment": "Validation message for unrecognized sensor code" + "Remove this CGM?": { + "comment": "Confirmation title for removing the CGM", + "extractionState": "manual" }, - "Connecting to your transmitter": { - "extractionState": "manual", - "comment": "Pairing screen heading" + "Replace sensor": { + "comment": "Detail when the session is over", + "extractionState": "manual" }, - "Your transmitter only talks to your phone about once every 5 minutes, so this can take a few minutes. Keep your phone nearby.": { - "extractionState": "manual", - "comment": "Pairing: explanation of the 5-minute cycle" + "Reported lifetime": { + "comment": "Transmitter detail label for reported lifetime", + "extractionState": "manual" + }, + "Resistance": { + "comment": "Transmitter detail label for battery resistance", + "extractionState": "manual" + }, + "Rub your fingers around the patch three times to secure it.": { + "comment": "Attach step 5 body", + "extractionState": "manual" + }, + "Save Credentials": { + "comment": "Button to save Share credentials", + "extractionState": "manual" + }, + "Scan package code": { + "comment": "Scan package barcode button", + "extractionState": "manual" + }, + "Scanned a Dexcom package, but couldn't find a transmitter ID in it. Type the ID manually.": { + "comment": "Scanned Dexcom barcode without a transmitter ID", + "extractionState": "manual" + }, + "Send Calibration": { + "comment": "Calibration submit button", + "extractionState": "manual" + }, + "Sensor": { + "comment": "Settings section: sensor", + "extractionState": "manual" + }, + "Sensor code": { + "comment": "Metadata label for sensor code", + "extractionState": "manual" }, - "Tap Pair on the iOS prompt": { - "extractionState": "manual", - "comment": "Pairing callout title while awaiting the prompt" + "Sensor Code": { + "comment": "Navigation title for sensor code entry", + "extractionState": "manual" }, - "Accept the Bluetooth pairing request within about a minute. If it disappears, the app tries again on the next connection.": { - "extractionState": "manual", - "comment": "Pairing callout body while awaiting the prompt" + "Sensor Expired": { + "comment": "Status highlight for expired sensor", + "extractionState": "manual" }, - "A pairing request will appear": { - "extractionState": "manual", - "comment": "Pairing callout title" + "Sensor failed": { + "comment": "Algorithm state: failed", + "extractionState": "manual" }, - "The first time they connect, iOS shows a Bluetooth pairing request. Tap Pair within about a minute — if it disappears, the app will try again on the next connection.": { - "extractionState": "manual", - "comment": "Pairing callout body about the iOS prompt" + "Sensor Failed": { + "comment": "Status highlight for failed sensor", + "extractionState": "manual" }, "Sensor session found": { - "extractionState": "manual", - "comment": "Pairing success label for an existing session" + "comment": "Pairing success label for an existing session", + "extractionState": "manual" }, - "Still looking": { - "extractionState": "manual", - "comment": "Pairing troubleshooting title" + "Sensor start queued — it is sent the next time the transmitter connects, which can take up to 5 minutes.": { + "comment": "Shown while a session start is waiting for the next connection", + "extractionState": "manual" }, - "Check that the Dexcom app is not connected to this transmitter, that the transmitter ID is exactly right, and that your phone is next to it. Bluetooth must be on.": { - "extractionState": "manual", - "comment": "Pairing troubleshooting body" + "Sensor started": { + "comment": "Metadata label for sensor start", + "extractionState": "manual" }, - "Share Logs": { - "extractionState": "manual", - "comment": "Row to export the log files" + "Sensor status": { + "comment": "Reading detail label for algorithm state", + "extractionState": "manual" }, - "Connected to your transmitter": { - "extractionState": "manual", - "comment": "Pairing status: connected" + "Sensor Warmup": { + "comment": "Status highlight during warm-up", + "extractionState": "manual" }, - "Waiting for you to accept pairing…": { - "extractionState": "manual", - "comment": "Pairing status: awaiting prompt" + "Session Ended": { + "comment": "Session phase: expired", + "extractionState": "manual" }, - "Looking for your transmitter…": { - "extractionState": "manual", - "comment": "Pairing status: searching" + "Session ends": { + "comment": "Metadata label for session end", + "extractionState": "manual" }, - "Your sensor is warming up": { - "extractionState": "manual", - "comment": "Warm-up screen heading" + "Session Length": { + "comment": "Navigation title for sensor life settings", + "extractionState": "manual" }, - "Warm-up takes about %d minutes. You will not get any glucose readings until it finishes.": { - "extractionState": "manual", - "comment": "Warm-up duration explanation (1: minutes)" + "Session length: %d days": { + "comment": "Session length value (1: day count)", + "extractionState": "manual" }, - "Do not dose on early readings": { - "extractionState": "manual", - "comment": "Warm-up callout title" + "Session stopped": { + "comment": "Algorithm state: stopped", + "extractionState": "manual" }, - "Do not make insulin decisions from the first readings after warm-up, or from readings that look wrong for how you feel. Use a fingerstick meter instead, and follow your care team's guidance.": { - "extractionState": "manual", - "comment": "Warm-up callout body about dosing safety" + "Share Logs": { + "comment": "Row to export the log files", + "extractionState": "manual" }, - "Trend": { - "extractionState": "manual", - "comment": "Reading detail label for trend" + "Signal Loss": { + "comment": "Status highlight for signal loss", + "extractionState": "manual" }, - "Sensor status": { - "extractionState": "manual", - "comment": "Reading detail label for algorithm state" + "Slide the transmitter tab into the slot at the narrow end of the holder.": { + "comment": "Attach step 3 body", + "extractionState": "manual" }, - "Display only": { - "extractionState": "manual", - "comment": "Reading detail label for display-only flag" + "Source": { + "comment": "Reading detail section header for source", + "extractionState": "manual" }, - "Yes": { - "extractionState": "manual", - "comment": "Affirmative" + "Start New Sensor": { + "comment": "Button to begin a new sensor session", + "extractionState": "manual" }, - "No": { - "extractionState": "manual", - "comment": "Negative" + "Start time unknown": { + "comment": "Detail when a session is running but its start is unknown", + "extractionState": "manual" }, - "Reading": { - "extractionState": "manual", - "comment": "Reading detail section header" + "Start Without a Code": { + "comment": "Button to start a session with no sensor code", + "extractionState": "manual" }, - "A display-only value has been shifted by a recent calibration. It is shown for continuity and is not used for dosing.": { - "extractionState": "manual", - "comment": "Footer explaining display-only readings" + "Start without a sensor code?": { + "comment": "Confirmation title for a code-less session", + "extractionState": "manual" }, - "Device": { - "extractionState": "manual", - "comment": "Reading detail label for device" + "STEP %d of 3": { + "comment": "Calibration step counter (1: step number)", + "extractionState": "manual" }, - "Identifier": { - "extractionState": "manual", - "comment": "Reading detail label for sync identifier" + "STEP %d of 4": { + "comment": "Attach-transmitter step counter (1: step number)", + "extractionState": "manual" }, - "Source": { - "extractionState": "manual", - "comment": "Reading detail section header for source" + "STEP %d of 10": { + "comment": "Insert-sensor step counter (1: step number)", + "extractionState": "manual" }, - "The identifier is derived from the transmitter's own clock, so the same reading keeps one identity whether it arrives live or is filled in later.": { - "extractionState": "manual", - "comment": "Footer explaining the sync identifier" + "Still looking": { + "comment": "Pairing troubleshooting title", + "extractionState": "manual" }, - "Latest Reading": { - "extractionState": "manual", - "comment": "Reading detail screen title" + "Stop Sensor Session": { + "comment": "Button to stop the sensor session", + "extractionState": "manual" }, - "Not available": { - "extractionState": "manual", - "comment": "Value when the transmitter reports no trend" + "Stop Session": { + "comment": "Confirm stop session", + "extractionState": "manual" }, - "Sensor failed": { - "extractionState": "manual", - "comment": "Algorithm state: failed" + "Stop this sensor session?": { + "comment": "Confirmation title for stopping a session", + "extractionState": "manual" }, - "Warming up": { - "extractionState": "manual", - "comment": "Algorithm state: warm-up" + "Take a blood sample from your fingertip and measure it with your meter.": { + "comment": "Calibration step 2 body", + "extractionState": "manual" }, - "Session stopped": { - "extractionState": "manual", - "comment": "Algorithm state: stopped" + "Take out the applicator whose sensor code you entered, and peel off its cover.": { + "comment": "Insert step 4 body", + "extractionState": "manual" }, - "Calibration requested": { - "extractionState": "manual", - "comment": "Algorithm state: needs calibration" + "Take the transmitter out of its box and prepare it for use. Wipe the bottom with an alcohol wipe and let it dry.": { + "comment": "Attach step 1 body", + "extractionState": "manual" }, - "OK": { - "extractionState": "manual", - "comment": "Algorithm state: ok" + "Tap Pair on the iOS prompt": { + "comment": "Pairing callout title while awaiting the prompt", + "extractionState": "manual" }, - "Unusable (code %d)": { - "extractionState": "manual", - "comment": "Algorithm state: unusable with raw code" + "Temperature": { + "comment": "Transmitter detail label for temperature", + "extractionState": "manual" }, - "Start time unknown": { - "extractionState": "manual", - "comment": "Detail when a session is running but its start is unknown" + "That code isn't one this app recognizes. Check the digits on the applicator label, or start without a code.": { + "comment": "Validation message for unrecognized sensor code", + "extractionState": "manual" }, - "%@ until ready": { - "extractionState": "manual", - "comment": "Warm-up remaining (1: duration)" + "That doesn't look like a Dexcom package. Try again or type the ID manually.": { + "comment": "Scanned non-Dexcom barcode", + "extractionState": "manual" }, - "Replace sensor": { - "extractionState": "manual", - "comment": "Detail when the session is over" + "The 4-digit code is on the applicator’s adhesive label, and is unique to that sensor. Use the code from the applicator you are about to insert — a code from a different one will make readings inaccurate.": { + "comment": "Sensor code: where to find it", + "extractionState": "manual" }, - "%@ remaining": { - "extractionState": "manual", - "comment": "Session remaining (1: duration)" + "The calibration is sent the next time your transmitter connects, which can be up to 5 minutes away. The transmitter may accept it, ask for a second value, or reject it — this screen's status will tell you which.": { + "comment": "Calibration callout body about delivery timing", + "extractionState": "manual" }, - "—": { - "extractionState": "manual", - "comment": "Placeholder for missing value" + "The camera couldn't start. Try again or type the ID manually.": { + "comment": "Scanner failed to start", + "extractionState": "manual" }, - "Just now": { - "extractionState": "manual", - "comment": "Reading received less than a minute ago" + "The first time they connect, iOS shows a Bluetooth pairing request. Tap Pair within about a minute — if it disappears, the app will try again on the next connection.": { + "comment": "Pairing callout body about the iOS prompt", + "extractionState": "manual" }, - "%d min ago": { - "extractionState": "manual", - "comment": "Minutes since last reading (1: minutes)" + "The identifier is derived from the transmitter's own clock, so the same reading keeps one identity whether it arrives live or is filled in later.": { + "comment": "Footer explaining the sync identifier", + "extractionState": "manual" }, - "%dd %dh": { - "extractionState": "manual", - "comment": "Duration in days and hours" + "The site must be completely dry or the adhesive will not stick.": { + "comment": "Insert step 2 note", + "extractionState": "manual" }, - "%dh %dm": { - "extractionState": "manual", - "comment": "Duration in hours and minutes" + "This app": { + "comment": "Fallback when the host app name cannot be read", + "extractionState": "manual" }, - "%dm": { - "extractionState": "manual", - "comment": "Duration in minutes" + "This app will stop reading from your transmitter. Your sensor session keeps running on the transmitter itself.": { + "comment": "Confirmation message for removing the CGM", + "extractionState": "manual" }, - "Sensor start queued — it is sent the next time the transmitter connects, which can take up to 5 minutes.": { - "extractionState": "manual", - "comment": "Shown while a session start is waiting for the next connection" + "This app works with Dexcom G6 and Dexcom ONE transmitters. Older G5 transmitters, whose IDs begin with 4, are not supported.": { + "comment": "Transmitter ID: supported hardware note", + "extractionState": "manual" }, - "Sensor": { - "extractionState": "manual", - "comment": "Settings section: sensor" + "This transmitter is past the end of its life. It still reports readings for a running session, but cannot start a new sensor.": { + "comment": "Footer shown when the transmitter has expired", + "extractionState": "manual" }, - "Last Reading": { - "extractionState": "manual", - "comment": "Settings section: last reading" + "This transmitter reports an extended 180-day lifetime, which means it has been modified. Warm-up is shorter and you can choose how long sessions run.": { + "comment": "Footer explaining Anubis detection", + "extractionState": "manual" }, - "No readings — replace the sensor.": { - "extractionState": "manual", - "comment": "Placeholder when the sensor has failed" + "Thoroughly wash and dry your hands.": { + "comment": "Insert step 1 body", + "extractionState": "manual" }, - "Warming up — readings start soon.": { - "extractionState": "manual", - "comment": "Placeholder during warm-up" + "Transmitter": { + "comment": "Navigation title for transmitter details", + "extractionState": "manual" }, - "Waiting for first reading…": { - "extractionState": "manual", - "comment": "Placeholder before the first reading" + "Transmitter Details": { + "comment": "Row to transmitter details", + "extractionState": "manual" }, - "Sensor started": { - "extractionState": "manual", - "comment": "Metadata label for sensor start" + "Transmitter Expired": { + "comment": "Status highlight when the transmitter is past end of life", + "extractionState": "manual" }, - "Session ends": { - "extractionState": "manual", - "comment": "Metadata label for session end" + "Transmitter ID": { + "comment": "Navigation title for transmitter ID entry", + "extractionState": "manual" }, - "Sensor code": { - "extractionState": "manual", - "comment": "Metadata label for sensor code" + "Trend": { + "comment": "Reading detail label for trend", + "extractionState": "manual" }, - "Not used": { - "extractionState": "manual", - "comment": "Value when a session runs without a sensor code" + "Turning this on sends your readings to Dexcom Share so people who follow you can see them in the Dexcom Follow app. It is optional, and your readings still work in this app without it.": { + "comment": "Footer explaining Share upload", + "extractionState": "manual" }, - "Start New Sensor": { - "extractionState": "manual", - "comment": "Button to begin a new sensor session" + "Unknown": { + "comment": "Placeholder for unknown value", + "extractionState": "manual" }, - "Enter Calibration": { - "extractionState": "manual", - "comment": "Button to enter a calibration" + "Unusable (code %d)": { + "comment": "Algorithm state: unusable with raw code", + "extractionState": "manual" }, - "After warm-up": { - "extractionState": "manual", - "comment": "Why calibration is unavailable during warm-up" + "Upload to Dexcom Share": { + "comment": "Toggle label for Share upload", + "extractionState": "manual" }, - "No sensor": { - "extractionState": "manual", - "comment": "Why calibration is unavailable without a session" + "Upload turned off and saved credentials removed.": { + "comment": "Status message after disabling Share upload", + "extractionState": "manual" }, - "Stop Sensor Session": { - "extractionState": "manual", - "comment": "Button to stop the sensor session" + "Use a fingertip only — samples from other sites are less accurate. Use the same meter throughout a sensor session, and check that your strips are in date.": { + "comment": "Calibration step 2 note", + "extractionState": "manual" }, - "Stop this sensor session?": { - "extractionState": "manual", - "comment": "Confirmation title for stopping a session" + "Use a fresh fingerstick reading from your meter, taken just now. Wash and dry your hands first — a value from unwashed fingers can be badly wrong and will teach the sensor the wrong thing.": { + "comment": "Calibration instructions", + "extractionState": "manual" }, - "Stop Session": { - "extractionState": "manual", - "comment": "Confirm stop session" + "Use This Code": { + "comment": "Sensor code submit button", + "extractionState": "manual" }, - "You will stop getting readings until you start a new sensor. A stopped session cannot be restarted.": { - "extractionState": "manual", - "comment": "Confirmation message for stopping a session" + "Username": { + "comment": "Share account username field", + "extractionState": "manual" }, - "Calibration becomes available once warm-up finishes. Do not make insulin decisions from early readings — use a fingerstick meter and follow your care team's guidance.": { - "extractionState": "manual", - "comment": "Footer explaining warm-up restrictions" + "Voltage A": { + "comment": "Transmitter detail label for battery A", + "extractionState": "manual" }, - "Transmitter Details": { - "extractionState": "manual", - "comment": "Row to transmitter details" + "Voltage B": { + "comment": "Transmitter detail label for battery B", + "extractionState": "manual" }, - "Dexcom Share Upload": { - "extractionState": "manual", - "comment": "Row to Share upload settings" + "Voltage B is getting low. The transmitter should finish the sensor you are wearing, but order a replacement now.": { + "comment": "Footer shown when battery B is low", + "extractionState": "manual" }, - "How to Apply a Sensor": { - "extractionState": "manual", - "comment": "Row to the placement guide" + "Voltage B is very low. This transmitter may stop sending readings at any time, even during a session. Replace it as soon as you can.": { + "comment": "Footer shown when battery B is very low", + "extractionState": "manual" }, - "Remove CGM from App": { - "extractionState": "manual", - "comment": "Button to delete the CGM manager" + "Waiting for first reading…": { + "comment": "Placeholder before the first reading", + "extractionState": "manual" }, - "Remove this CGM?": { - "extractionState": "manual", - "comment": "Confirmation title for removing the CGM" + "Waiting for you to accept pairing…": { + "comment": "Pairing status: awaiting prompt", + "extractionState": "manual" }, - "Remove CGM": { - "extractionState": "manual", - "comment": "Confirm CGM removal" + "Warm-Up": { + "comment": "Navigation title for warm-up screen", + "extractionState": "manual" }, - "This app will stop reading from your transmitter. Your sensor session keeps running on the transmitter itself.": { - "extractionState": "manual", - "comment": "Confirmation message for removing the CGM" + "Warm-up is finished": { + "comment": "Warm-up screen heading once warm-up has completed", + "extractionState": "manual" }, - "“Dexcom”, “G6”, and “ONE” are trademarks of Dexcom, Inc., used here only to describe which devices are compatible. This app is not made by or endorsed by Dexcom.": { - "extractionState": "manual", - "comment": "Trademark attribution footer" + "Warm-up takes about %d minutes. You will not get any glucose readings until it finishes.": { + "comment": "Warm-up duration explanation (1: minutes)", + "extractionState": "manual" }, - "Warm-up is finished": { - "extractionState": "manual", - "comment": "Warm-up screen heading once warm-up has completed" + "Warming up": { + "comment": "Algorithm state: warm-up", + "extractionState": "manual" }, - "Readings will appear as they arrive from the transmitter, about every 5 minutes.": { - "extractionState": "manual", - "comment": "Explanation shown once warm-up has completed" + "Warming Up": { + "comment": "Session phase: warm-up", + "extractionState": "manual" }, - "Expected to finish at %1$@, about %2$d minutes from now": { - "extractionState": "manual", - "comment": "Warm-up completion time (1: time, 2: minutes remaining)" + "Warming up — readings start soon.": { + "comment": "Placeholder during warm-up", + "extractionState": "manual" }, - "Calibrations must be between %1$@ and %2$@. If your meter reads outside that range, treat it and follow your care team's guidance instead of calibrating.": { - "extractionState": "manual", - "comment": "Validation message for out-of-range calibration (1: minimum with unit, 2: maximum with unit)" + "Wash and dry your hands with soap and water — not a gel cleaner.": { + "comment": "Calibration step 1 body", + "extractionState": "manual" }, - "Voltage A": { - "extractionState": "manual", - "comment": "Transmitter detail label for battery A" + "Without the code the sensor cannot use its factory calibration, and the transmitter will ask you for two fingerstick values after warm-up and one every day after that. The code is on the applicator label.": { + "comment": "Confirmation message explaining a code-less session", + "extractionState": "manual" }, - "%d mV": { - "extractionState": "manual", - "comment": "Voltage in millivolts (1: millivolts)" + "Yes": { + "comment": "Affirmative", + "extractionState": "manual" }, - "Voltage B": { - "extractionState": "manual", - "comment": "Transmitter detail label for battery B" + "You are almost done — the sensor session starts next.": { + "comment": "Attach step 5 note", + "extractionState": "manual" }, - "Resistance": { - "extractionState": "manual", - "comment": "Transmitter detail label for battery resistance" + "You can start without one. The transmitter will then ask you for two fingerstick glucose values after warm-up, and for one more each day. \"Calibration\" means telling the sensor what your meter reads, so it can correct itself.": { + "comment": "Sensor code callout body explaining calibration", + "extractionState": "manual" }, - "Temperature": { - "extractionState": "manual", - "comment": "Transmitter detail label for temperature" + "You do not need the Dexcom app — and you must not let it use the same transmitter at the same time.": { + "comment": "Introduction: single-app requirement", + "extractionState": "manual" }, - "%d °C": { - "extractionState": "manual", - "comment": "Temperature in Celsius (1: degrees)" + "You will enter the new transmitter's ID and can then start a sensor session on it. Your other settings are kept.": { + "comment": "Confirmation message for pairing a new transmitter with no session running", + "extractionState": "manual" }, - "Last checked": { - "extractionState": "manual", - "comment": "Transmitter detail label for last battery read" + "You will stop getting readings until you start a new sensor. A stopped session cannot be restarted.": { + "comment": "Confirmation message for stopping a session", + "extractionState": "manual" }, - "Battery": { - "extractionState": "manual", - "comment": "Transmitter details section header for battery" + "Your current sensor session will end. A session lives on the transmitter, so it cannot move to a new one — you will start a fresh session after pairing, and can reuse the sensor you are wearing. Stop the sensor first if your old transmitter is still attached and you want it stopped cleanly. Your other settings are kept.": { + "comment": "Confirmation message for pairing a new transmitter while a session is running", + "extractionState": "manual" }, - "Voltage B is very low. This transmitter may stop sending readings at any time, even during a session. Replace it as soon as you can.": { - "extractionState": "manual", - "comment": "Footer shown when battery B is very low" + "Your sensor is warming up": { + "comment": "Warm-up screen heading", + "extractionState": "manual" }, - "Voltage B is getting low. The transmitter should finish the sensor you are wearing, but order a replacement now.": { + "Your transmitter only talks to your phone about once every 5 minutes, so this can take a few minutes. Keep your phone nearby.": { + "comment": "Pairing: explanation of the 5-minute cycle", + "extractionState": "manual" + }, + "Your username and password are stored in the iOS Keychain on this device only. They are never written into app settings, logs, or backups of this app's data.": { + "comment": "Footer explaining Keychain credential storage", + "extractionState": "manual" + }, + "Scan applicator code": { "extractionState": "manual", - "comment": "Footer shown when battery B is low" + "comment": "Scan applicator barcode button" }, - "Battery B is the cell that runs down first and determines whether the transmitter can finish a session.": { + "Allow camera access in Settings to scan the Data Matrix code on your sensor applicator.": { "extractionState": "manual", - "comment": "Footer explaining what battery B means" + "comment": "Camera denied alert message (sensor code)" }, - "%1$@%2$@ per minute": { + "Couldn't find a sensor code in that barcode. The code is in the Data Matrix on the applicator, not the sensor box.": { "extractionState": "manual", - "comment": "Trend rate (1: sign, 2: formatted rate with unit)" + "comment": "Scanned barcode without a sensor code" }, - "%1$@%2$@/min": { + "The camera couldn't start. Try again or type the code manually.": { "extractionState": "manual", - "comment": "Trend rate (1: sign, 2: formatted rate with unit)" + "comment": "Scanner failed to start" } }, - "version": "1.0" + "version": "1.1" } diff --git a/G6SensorKitUI/Views/G6OnboardingViews.swift b/G6SensorKitUI/Views/G6OnboardingViews.swift index 7655934..34a81ae 100644 --- a/G6SensorKitUI/Views/G6OnboardingViews.swift +++ b/G6SensorKitUI/Views/G6OnboardingViews.swift @@ -19,6 +19,21 @@ import LoopKitUI import G6SensorKit import G6SensorCore +#if targetEnvironment(simulator) +/// Simulator stand-in for the camera scanner, which needs hardware the +/// simulator lacks: the scan buttons stay visible there and deliver a +/// fixed payload, so the whole scan flow can be exercised without a +/// device. Compiled out on hardware. Both payloads are real, public +/// Dexcom packaging data (the same ones the parser tests use). +enum G6DemoScan { + /// The full Data Matrix from a public G6 transmitter box photo. + static let transmitterBox = G6PackageScan(payload: + "241STT-OM-001\u{1D}1018038762\u{1D}2188H03B\u{1D}17250226") + /// A real, public G6 applicator label. + static let applicator = G6PackageScan(payload: "10731863521434687D2405937") +} +#endif + // MARK: - Shared building blocks /// A numbered instruction row used by the placement guide. @@ -74,6 +89,75 @@ struct G6ContinueButton: View { } } +/// Large per-character boxes over a hidden text field that does the real +/// input. Shared by the transmitter-ID screen (6 boxes, alphanumeric) +/// and the sensor-code screen (4 boxes, numeric). +struct G6CodeBoxField: View { + enum AllowedCharacters { + case alphanumericUppercased + case numeric + } + + @Binding var text: String + let length: Int + let allowedCharacters: AllowedCharacters + var focused: FocusState.Binding + + private var filteredText: Binding { + Binding( + get: { text }, + set: { newValue in + let filtered: String + switch allowedCharacters { + case .alphanumericUppercased: + filtered = newValue.filter { $0.isLetter || $0.isNumber }.uppercased() + case .numeric: + filtered = newValue.filter(\.isNumber) + } + text = String(filtered.prefix(length)) + } + ) + } + + var body: some View { + ZStack { + TextField("", text: filteredText) + .focused(focused) + .keyboardType(allowedCharacters == .alphanumericUppercased ? .asciiCapable : .numberPad) + .autocorrectionDisabled() + .textInputAutocapitalization(allowedCharacters == .alphanumericUppercased ? .characters : .never) + .foregroundColor(.clear) + .accentColor(.clear) + .opacity(0.02) + + HStack(spacing: 10) { + ForEach(0 ..< length, id: \.self) { index in + let characters = Array(text) + let character = index < characters.count ? String(characters[index]) : "" + let isActive = index == characters.count + + Text(character) + .font(.title.weight(.semibold).monospaced()) + .frame(maxWidth: .infinity) + .frame(height: 64) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(.secondarySystemGroupedBackground)) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(isActive ? Color.accentColor : Color(.separator), lineWidth: isActive ? 2 : 1) + ) + } + } + .allowsHitTesting(false) + } + .frame(height: 64) + .contentShape(Rectangle()) + .onTapGesture { focused.wrappedValue = true } + } +} + // MARK: - 1. Introduction /// Product hero, in the shape G7SensorKit uses: name, device, a short @@ -172,6 +256,10 @@ struct G6TransmitterIDEntryView: View { @State private var transmitterID: String = "" @State private var validationMessage: String? + @State private var showScanner = false + @State private var showCameraDeniedAlert = false + @State private var scanFailedMessage: String? + @FocusState private var fieldFocused: Bool init(initialValue: String, didSubmit: @escaping (String) -> Void) { self.initialValue = initialValue @@ -202,24 +290,42 @@ struct G6TransmitterIDEntryView: View { G6TransmitterBackGlyph(size: 150) } - TextField(LocalizedString("6-character ID", comment: "Transmitter ID text field placeholder"), text: $transmitterID) - .textCase(.uppercase) - .autocorrectionDisabled() - .textInputAutocapitalization(.characters) - .font(.title3.monospaced()) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(10) - .onChange(of: transmitterID) { newValue in - transmitterID = String(newValue.uppercased().prefix(6)) - validationMessage = nil - } + G6CodeBoxField( + text: $transmitterID, + length: 6, + allowedCharacters: .alphanumericUppercased, + focused: $fieldFocused + ) + .frame(maxWidth: .infinity) + .onChange(of: transmitterID) { + validationMessage = nil + autoSubmitIfComplete() + } - if let validationMessage = validationMessage { - Text(validationMessage) - .font(.subheadline) - .foregroundColor(.red) - .fixedSize(horizontal: false, vertical: true) + VStack(spacing: 6) { + if let validationMessage = validationMessage { + Text(validationMessage) + .foregroundColor(.red) + } + if let scanFailedMessage = scanFailedMessage { + Text(verbatim: scanFailedMessage) + .foregroundColor(.red) + } + } + .font(.subheadline) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) + + if isScanAvailable { + Button(action: scanTapped) { + HStack(spacing: 8) { + Image(systemName: "barcode.viewfinder") + Text(LocalizedString("Scan package code", comment: "Scan package barcode button")) + } + .font(.subheadline.weight(.semibold)) + } + .frame(maxWidth: .infinity) } Text(LocalizedString("This app works with Dexcom G6 and Dexcom ONE transmitters. Older G5 transmitters, whose IDs begin with 4, are not supported.", comment: "Transmitter ID: supported hardware note")) @@ -229,11 +335,120 @@ struct G6TransmitterIDEntryView: View { } .padding() } + .onTapGesture { fieldFocused = false } G6ContinueButton(isEnabled: transmitterID.count == 6) { submit() } } + .sheet(isPresented: $showScanner) { scannerSheet } + .alert( + Text(LocalizedString("Camera access is off", comment: "Camera denied alert title")), + isPresented: $showCameraDeniedAlert + ) { + Button(action: { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + }) { Text(LocalizedString("Open Settings", comment: "Open Settings button")) } + Button(role: .cancel, action: {}) { Text(LocalizedString("Cancel", comment: "Cancel button")) } + } message: { + Text(LocalizedString("Allow camera access in Settings to scan the Data Matrix code on your Dexcom transmitter packaging.", comment: "Camera denied alert message")) + } + } + + /// A complete ID submits itself, typed or scanned: with the boxes full + /// there is nothing left to enter. A short delay lets the last box + /// visibly fill first. + private func autoSubmitIfComplete() { + guard transmitterID.count == 6, !isG5 else { + return + } + fieldFocused = false + let submitted = transmitterID + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + guard transmitterID == submitted else { return } + didSubmit(submitted) + } + } + + // MARK: - Package scanning + + /// The camera scanner needs hardware support and a camera usage + /// description; the simulator gets the demo-payload stand-in instead + /// (see `G6DemoScan`). + private var isScanAvailable: Bool { + #if targetEnvironment(simulator) + return true + #else + return G6PackageScannerView.isSupported + #endif + } + + private func scanTapped() { + #if targetEnvironment(simulator) + handleScan(G6DemoScan.transmitterBox) + #else + switch G6PackageScannerView.cameraAuthorization { + case .authorized: + showScanner = true + case .notDetermined: + G6PackageScannerView.requestCameraAccess { granted in + if granted { + showScanner = true + } else { + showCameraDeniedAlert = true + } + } + default: + showCameraDeniedAlert = true + } + #endif + } + + private func handleScan(_ scan: G6PackageScan) { + showScanner = false + scanFailedMessage = nil + + // A usable ID outweighs the GTIN check: the transmitter box's own + // Data Matrices carry no (01) — the GTIN sits on a separate linear + // barcode — so gating on isDexcomPackage would reject the box. + guard let candidate = scan.transmitterIDCandidate else { + scanFailedMessage = scan.isDexcomPackage + ? LocalizedString("Scanned a Dexcom package, but couldn't find a transmitter ID in it. Type the ID manually.", comment: "Scanned Dexcom barcode without a transmitter ID") + : LocalizedString("That doesn't look like a Dexcom package. Try again or type the ID manually.", comment: "Scanned non-Dexcom barcode") + return + } + + // Assigning the field fires the auto-submit path, same as typing. + transmitterID = candidate + } + + private func handleScanStartFailure(_: Error) { + showScanner = false + scanFailedMessage = LocalizedString("The camera couldn't start. Try again or type the ID manually.", comment: "Scanner failed to start") + } + + private var scannerSheet: some View { + NavigationView { + G6PackageScannerView(onScan: handleScan, onStartFailure: handleScanStartFailure) + .edgesIgnoringSafeArea(.all) + .navigationTitle(Text(LocalizedString("Scan package code", comment: "Scanner screen title"))) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { showScanner = false }) { + Text(LocalizedString("Cancel", comment: "Cancel button")) + } + } + } + } + } + + // MARK: - Submit + + private var isG5: Bool { + return transmitterID.hasPrefix("4") } private func submit() { @@ -241,7 +456,7 @@ struct G6TransmitterIDEntryView: View { return } - if transmitterID.hasPrefix("4") { + if isG5 { validationMessage = LocalizedString("IDs that begin with 4 belong to Dexcom G5 transmitters, which this app does not support.", comment: "Validation message for G5 transmitter ID") return } @@ -259,6 +474,9 @@ struct G6SensorCodeEntryView: View { @State private var sensorCode: String = "" @State private var validationMessage: String? @State private var confirmingNoCode = false + @State private var showScanner = false + @State private var showCameraDeniedAlert = false + @State private var scanFailedMessage: String? @FocusState private var codeFieldFocused: Bool var body: some View { @@ -275,23 +493,43 @@ struct G6SensorCodeEntryView: View { G6SensorCodeLabelGlyph(size: 150) } - TextField(LocalizedString("4-digit code", comment: "Sensor code text field placeholder"), text: $sensorCode) - .keyboardType(.numberPad) - .focused($codeFieldFocused) - .font(.title3.monospaced()) - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(10) - .onChange(of: sensorCode) { newValue in - sensorCode = String(newValue.filter(\.isNumber).prefix(4)) - validationMessage = nil - } + G6CodeBoxField( + text: $sensorCode, + length: 4, + allowedCharacters: .numeric, + focused: $codeFieldFocused + ) + .frame(maxWidth: .infinity) + .padding(.horizontal, 32) + .onChange(of: sensorCode) { + validationMessage = nil + autoSubmitIfComplete() + } - if let validationMessage = validationMessage { - Text(validationMessage) - .font(.subheadline) - .foregroundColor(.red) - .fixedSize(horizontal: false, vertical: true) + VStack(spacing: 6) { + if let validationMessage = validationMessage { + Text(validationMessage) + .foregroundColor(.red) + } + if let scanFailedMessage = scanFailedMessage { + Text(verbatim: scanFailedMessage) + .foregroundColor(.red) + } + } + .font(.subheadline) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) + + if isScanAvailable { + Button(action: scanTapped) { + HStack(spacing: 8) { + Image(systemName: "barcode.viewfinder") + Text(LocalizedString("Scan applicator code", comment: "Scan applicator barcode button")) + } + .font(.subheadline.weight(.semibold)) + } + .frame(maxWidth: .infinity) } G6CalloutBox( @@ -304,7 +542,7 @@ struct G6SensorCodeEntryView: View { } .padding() } - .onAppear { codeFieldFocused = true } + .onTapGesture { codeFieldFocused = false } VStack(spacing: 10) { G6ContinueButton( @@ -334,8 +572,114 @@ struct G6SensorCodeEntryView: View { } } } + .sheet(isPresented: $showScanner) { scannerSheet } + .alert( + Text(LocalizedString("Camera access is off", comment: "Camera denied alert title")), + isPresented: $showCameraDeniedAlert + ) { + Button(action: { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + }) { Text(LocalizedString("Open Settings", comment: "Open Settings button")) } + Button(role: .cancel, action: {}) { Text(LocalizedString("Cancel", comment: "Cancel button")) } + } message: { + Text(LocalizedString("Allow camera access in Settings to scan the Data Matrix code on your sensor applicator.", comment: "Camera denied alert message (sensor code)")) + } + } + + /// A complete code submits itself when it is a known factory code, + /// typed or scanned. Unknown codes show the validation message instead + /// of advancing. + private func autoSubmitIfComplete() { + guard sensorCode.count == 4 else { + return + } + guard SensorCode(sensorCode) != nil else { + validationMessage = LocalizedString("That code isn't one this app recognizes. Check the digits on the applicator label, or start without a code.", comment: "Validation message for unrecognized sensor code") + return + } + codeFieldFocused = false + let submitted = sensorCode + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + guard sensorCode == submitted else { return } + didSubmit(submitted) + } + } + + // MARK: - Package scanning + + /// The camera scanner needs hardware support and a camera usage + /// description; the simulator gets the demo-payload stand-in instead + /// (see `G6DemoScan`). + private var isScanAvailable: Bool { + #if targetEnvironment(simulator) + return true + #else + return G6PackageScannerView.isSupported + #endif + } + + private func scanTapped() { + #if targetEnvironment(simulator) + handleScan(G6DemoScan.applicator) + #else + switch G6PackageScannerView.cameraAuthorization { + case .authorized: + showScanner = true + case .notDetermined: + G6PackageScannerView.requestCameraAccess { granted in + if granted { + showScanner = true + } else { + showCameraDeniedAlert = true + } + } + default: + showCameraDeniedAlert = true + } + #endif } + private func handleScan(_ scan: G6PackageScan) { + showScanner = false + scanFailedMessage = nil + + // Applicator labels carry no GTIN, so unlike the transmitter-ID + // screen there is no Dexcom-package gate: either a sensor code + // falls out of the payload or it does not. + guard let candidate = scan.sensorCodeCandidate else { + scanFailedMessage = LocalizedString("Couldn't find a sensor code in that barcode. The code is in the Data Matrix on the applicator, not the sensor box.", comment: "Scanned barcode without a sensor code") + return + } + + // Assigning the field fires the auto-submit path, same as typing. + sensorCode = candidate + } + + private func handleScanStartFailure(_: Error) { + showScanner = false + scanFailedMessage = LocalizedString("The camera couldn't start. Try again or type the code manually.", comment: "Scanner failed to start") + } + + private var scannerSheet: some View { + NavigationView { + G6PackageScannerView(onScan: handleScan, onStartFailure: handleScanStartFailure) + .edgesIgnoringSafeArea(.all) + .navigationTitle(Text(LocalizedString("Scan applicator code", comment: "Scanner screen title (sensor code)"))) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { showScanner = false }) { + Text(LocalizedString("Cancel", comment: "Cancel button")) + } + } + } + } + } + + // MARK: - Submit + private func submit() { guard SensorCode(sensorCode) != nil else { validationMessage = LocalizedString("That code isn't one this app recognizes. Check the digits on the applicator label, or start without a code.", comment: "Validation message for unrecognized sensor code") diff --git a/G6SensorKitUI/Views/G6PackageScannerView.swift b/G6SensorKitUI/Views/G6PackageScannerView.swift new file mode 100644 index 0000000..eb0f057 --- /dev/null +++ b/G6SensorKitUI/Views/G6PackageScannerView.swift @@ -0,0 +1,85 @@ +// +// G6PackageScannerView.swift +// G6SensorKitUI +// +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import AVFoundation +import G6SensorCore +import SwiftUI +import VisionKit + +/// Camera scanner for the GS1 Data Matrix barcodes on Dexcom packaging: +/// the applicator label and the transmitter box. The box carries two Data +/// Matrices — serial-only (21) and the full (241)(10)(21)(17) — and +/// either yields the transmitter ID. +struct G6PackageScannerView: UIViewControllerRepresentable { + let onScan: (G6PackageScan) -> Void + let onStartFailure: (Error) -> Void + + static var isSupported: Bool { + return DataScannerViewController.isSupported + && Bundle.main.object(forInfoDictionaryKey: "NSCameraUsageDescription") != nil + } + + static var cameraAuthorization: AVAuthorizationStatus { + return AVCaptureDevice.authorizationStatus(for: .video) + } + + static func requestCameraAccess(completion: @escaping (Bool) -> Void) { + AVCaptureDevice.requestAccess(for: .video) { granted in + DispatchQueue.main.async { completion(granted) } + } + } + + func makeUIViewController(context: Context) -> DataScannerViewController { + let scanner = DataScannerViewController( + recognizedDataTypes: [.barcode(symbologies: [.dataMatrix])], + qualityLevel: .balanced, + recognizesMultipleItems: false, + isHighFrameRateTrackingEnabled: false, + isHighlightingEnabled: true + ) + scanner.delegate = context.coordinator + do { + try scanner.startScanning() + } catch { + DispatchQueue.main.async { onStartFailure(error) } + } + return scanner + } + + func updateUIViewController(_: DataScannerViewController, context _: Context) {} + + func makeCoordinator() -> Coordinator { + return Coordinator(onScan: onScan) + } + + final class Coordinator: NSObject, DataScannerViewControllerDelegate { + private let onScan: (G6PackageScan) -> Void + private var didDeliver = false + + init(onScan: @escaping (G6PackageScan) -> Void) { + self.onScan = onScan + } + + func dataScanner( + _ dataScanner: DataScannerViewController, + didAdd addedItems: [RecognizedItem], + allItems _: [RecognizedItem] + ) { + guard !didDeliver else { return } + for item in addedItems { + guard case let .barcode(barcode) = item, + let payload = barcode.payloadStringValue + else { continue } + didDeliver = true + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + onScan(G6PackageScan(payload: payload)) + dataScanner.stopScanning() + return + } + } + } +} diff --git a/G6SensorKitUI/Views/G6SettingsView.swift b/G6SensorKitUI/Views/G6SettingsView.swift index 1cc7742..ddbcc27 100644 --- a/G6SensorKitUI/Views/G6SettingsView.swift +++ b/G6SensorKitUI/Views/G6SettingsView.swift @@ -151,15 +151,17 @@ final class G6SettingsViewModel: ObservableObject, G6CGMManagerObserver { } var trendSymbol: String? { - guard let rate = state.latestReading?.trendRateMgDLPerMinute, !isSignalLost else { + guard !isSignalLost, + let arrow = G6TrendArrow(dexcomRateMgDLPerMinute: state.latestReading?.trendRateMgDLPerMinute) + else { return nil } - switch rate { - case ..<(-3): return "arrow.down" - case ..<(-1): return "arrow.down.right" - case ..<1: return "arrow.right" - case ..<3: return "arrow.up.right" - default: return "arrow.up" + switch arrow { + case .downDownDown, .downDown: return "arrow.down" + case .down: return "arrow.down.right" + case .flat: return "arrow.right" + case .up, .upUp: return "arrow.up.right" + case .upUpUp: return "arrow.up" } } diff --git a/Package.swift b/Package.swift index 79005f4..815577b 100644 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,7 @@ import PackageDescription let package = Package( name: "G6SensorCore", platforms: [ - .iOS(.v15), + .iOS(.v17), .macOS(.v13), ], products: [ diff --git a/Scripts/generate_project.rb b/Scripts/generate_project.rb index e0b9b6f..5ce1b7b 100644 --- a/Scripts/generate_project.rb +++ b/Scripts/generate_project.rb @@ -20,7 +20,7 @@ ROOT = File.expand_path('..', __dir__) PROJECT_PATH = File.join(ROOT, 'G6SensorKit.xcodeproj') -DEPLOYMENT_TARGET = '15.1' +DEPLOYMENT_TARGET = '17.6' SWIFT_VERSION = '5.9' BUNDLE_PREFIX = 'org.nightscout' diff --git a/Sources/G6SensorCore/Auth/G6TransmitterModel.swift b/Sources/G6SensorCore/Auth/G6TransmitterModel.swift new file mode 100644 index 0000000..0558c08 --- /dev/null +++ b/Sources/G6SensorCore/Auth/G6TransmitterModel.swift @@ -0,0 +1,27 @@ +// +// G6TransmitterModel.swift +// G6SensorKit +// +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import Foundation + +/// The G6-family model, classified from the transmitter ID the user entered. +public enum G6TransmitterModel: CaseIterable { + case g6 + case one + + /// Dexcom ONE transmitters use `5…` or `C…`; G6 uses `8…`. An unrecognized + /// prefix falls back to G6, the stock case. + public init(transmitterID: String) { + switch transmitterID.first { + case "5", + "c", + "C": + self = .one + default: + self = .g6 + } + } +} diff --git a/Sources/G6SensorCore/Auth/TransmitterID.swift b/Sources/G6SensorCore/Auth/TransmitterID.swift index f81a1eb..cebeffe 100644 --- a/Sources/G6SensorCore/Auth/TransmitterID.swift +++ b/Sources/G6SensorCore/Auth/TransmitterID.swift @@ -21,6 +21,11 @@ public struct TransmitterID { self.id = id } + /// The G6-family model, classified from the transmitter ID prefix. + public var model: G6TransmitterModel { + return G6TransmitterModel(transmitterID: id) + } + private var cryptKey: Data? { return "00\(id)00\(id)".data(using: .utf8) } diff --git a/Sources/G6SensorCore/Model/G6TrendArrow.swift b/Sources/G6SensorCore/Model/G6TrendArrow.swift new file mode 100644 index 0000000..95ad0e7 --- /dev/null +++ b/Sources/G6SensorCore/Model/G6TrendArrow.swift @@ -0,0 +1,50 @@ +// +// G6TrendArrow.swift +// G6SensorKit +// +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import Foundation + +/// The Dexcom trend arrow for a reported rate of change. +/// +/// The raw values match LoopKit's `GlucoseTrend` so the integration layer +/// can bridge with `GlucoseTrend(rawValue:)`. +public enum G6TrendArrow: Int, CaseIterable { + case upUpUp = 1 + case upUp = 2 + case up = 3 + case flat = 4 + case down = 5 + case downDown = 6 + case downDownDown = 7 + + /// Beyond this the official apps show no arrow at all. Reachable: the + /// rate arrives as Int8 tenths (0x7F is the "no trend" sentinel), so + /// roughly -12.8 to +12.6 comes in over the wire. + private static let dexcomArrowLimit = 8.0 + + /// The Dexcom arrow for the rate the transmitter reported, in + /// (mg/dL)/min. + /// + /// Thresholds match the apps' shared trend-arrow table: every threshold + /// belongs to the steeper arrow, `flat` excludes both of its ends, and + /// |rate| above 8 is no arrow. The transmitter sends the rate; the + /// receiver only buckets it. + public init?(dexcomRateMgDLPerMinute rate: Double?) { + guard let rate = rate, + (-Self.dexcomArrowLimit ... Self.dexcomArrowLimit).contains(rate) + else { return nil } + + switch rate { + case ...(-3): self = .downDownDown + case ...(-2): self = .downDown + case ...(-1): self = .down + case ..<1: self = .flat + case ..<2: self = .up + case ..<3: self = .upUp + default: self = .upUpUp + } + } +} diff --git a/Sources/G6SensorCore/Model/TransmitterManagerState.swift b/Sources/G6SensorCore/Model/TransmitterManagerState.swift index 6511a7e..cd57195 100644 --- a/Sources/G6SensorCore/Model/TransmitterManagerState.swift +++ b/Sources/G6SensorCore/Model/TransmitterManagerState.swift @@ -69,7 +69,7 @@ public struct TransmitterManagerState: RawRepresentable, Equatable { let sensorStartOffset = rawValue["sensorStartOffset"] as? UInt32 let transmitterExpiryInDays = (rawValue["transmitterExpiryInDays"] as? UInt16) - ?? (rawValue["transmitterExpiryInDays"] as? Int).map { UInt16($0) } + ?? (rawValue["transmitterExpiryInDays"] as? Int).map { UInt16(clamping: $0) } let sensorLifeDays = rawValue["sensorLifeDays"] as? Int ?? Self.defaultSensorLifeDays diff --git a/Sources/G6SensorCore/Pairing/GS1DataMatrixParser.swift b/Sources/G6SensorCore/Pairing/GS1DataMatrixParser.swift new file mode 100644 index 0000000..e161428 --- /dev/null +++ b/Sources/G6SensorCore/Pairing/GS1DataMatrixParser.swift @@ -0,0 +1,207 @@ +// +// GS1DataMatrixParser.swift +// G6SensorKit +// +// Ported from DexKit (Pairing/GS1DataMatrixParser.swift), trimmed to G6 +// and extended with applicator sensor-code extraction (AI 240) and +// FNC1-stripped payload fallbacks. +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import Foundation + +/// One GS1 element string: application identifier + value. +public struct GS1Element: Equatable { + public let ai: String + public let value: String +} + +/// Pure GS1 Data Matrix parser for Dexcom packaging barcodes. +/// +/// The Data Matrix on Dexcom packaging encodes (GS1 element structure, +/// verified against our own captures of G6/ONE packaging): +/// - AI (01) GTIN: 14 digits; Dexcom company prefix `0038627` +/// - AI (21) serial: G6: 6-char transmitter ID; G7: 12-char sensor serial +/// - AI (240) additional product ID: G6: the 4-digit sensor code on the +/// applicator label; G7: the 4-digit pairing code +/// - AI (10) lot, AI (11) production date, AI (17) expiry +/// Variable-length AIs are terminated by FNC1 (ASCII GS, 0x1D) when more +/// elements follow. +public enum GS1DataMatrixParser { + /// GS1 FNC1 separator as delivered in scanned payload strings. + public static let fnc1: Character = "\u{1D}" + + private struct AIInfo { + let fixedLength: Int? + let maxLength: Int + } + + /// AI registry: fixed-length AIs never need a trailing FNC1; variable + /// ones are read up to FNC1 or end of payload. + private static let registry: [String: AIInfo] = [ + "01": AIInfo(fixedLength: 14, maxLength: 14), // GTIN + "11": AIInfo(fixedLength: 6, maxLength: 6), // production date YYMMDD + "17": AIInfo(fixedLength: 6, maxLength: 6), // expiry YYMMDD + "10": AIInfo(fixedLength: nil, maxLength: 20), // lot/batch + "21": AIInfo(fixedLength: nil, maxLength: 20), // serial + "240": AIInfo(fixedLength: nil, maxLength: 30), // additional product ID (G6 sensor code / G7 pairing code) + "241": AIInfo(fixedLength: nil, maxLength: 30), // customer part number + "250": AIInfo(fixedLength: nil, maxLength: 30) // secondary serial + ] + + /// Parses a scanned payload into GS1 elements. Tolerates the AIM + /// symbology identifier ("]d1"/"]d2") and/or a leading FNC1 that some + /// scanners prepend. Stops at the first unrecognized AI and keeps + /// everything parsed up to that point. + public static func parse(_ payload: String) -> [GS1Element] { + var remaining = Substring(normalize(payload)) + var elements: [GS1Element] = [] + + while !remaining.isEmpty { + // Skip stray FNC1 separators (robustness). + if remaining.first == fnc1 { + remaining = remaining.dropFirst() + continue + } + + guard let (ai, info) = readAI(remaining) else { + break + } + remaining = remaining.dropFirst(ai.count) + + let value: Substring + if let fixed = info.fixedLength { + guard remaining.count >= fixed else { break } + value = remaining.prefix(fixed) + remaining = remaining.dropFirst(fixed) + } else { + let end = remaining.firstIndex(of: fnc1) ?? remaining.endIndex + value = remaining[.. info.maxLength { + break + } + } + elements.append(GS1Element(ai: ai, value: String(value))) + } + + return elements + } + + /// Strips the AIM symbology identifier and any leading FNC1. + static func normalize(_ payload: String) -> String { + var s = payload.trimmingCharacters(in: .whitespacesAndNewlines) + if s.hasPrefix("]d") { + // AIM identifier for Data Matrix: ]d1 = plain, ]d2 = GS1. + s = String(s.dropFirst(3)) + } + while s.first == fnc1 { + s = String(s.dropFirst()) + } + return s + } + + /// Longest-match AI read: 4 digits, then 3, then 2. + private static func readAI(_ s: Substring) -> (String, AIInfo)? { + for length in [4, 3, 2] { + guard s.count >= length else { continue } + let candidate = String(s.prefix(length)) + guard candidate.allSatisfy(\.isNumber) else { continue } + if let info = registry[candidate] { + return (candidate, info) + } + } + return nil + } +} + +/// A scanned Dexcom package, mapped to what G6 onboarding needs. +public struct G6PackageScan: Equatable { + public let elements: [GS1Element] + + /// The normalized payload, kept for the fallback candidate extraction: + /// some scanners strip the FNC1 separators, leaving one long string the + /// strict element parse cannot segment. + private let normalizedPayload: String + + public init(payload: String) { + normalizedPayload = GS1DataMatrixParser.normalize(payload) + elements = GS1DataMatrixParser.parse(payload) + } + + public func value(forAI ai: String) -> String? { + elements.first(where: { $0.ai == ai })?.value + } + + /// AI (01); identifies the product model. + public var gtin: String? { value(forAI: "01") } + /// AI (10) lot/batch. + public var lot: String? { value(forAI: "10") } + /// AI (21); G6: 6-char transmitter ID; G7: 12-char sensor serial. + public var serial: String? { value(forAI: "21") } + + /// Dexcom's GS1 company prefix (0038627…) inside the GTIN. + public var isDexcomPackage: Bool { + gtin?.hasPrefix("0038627") == true + } + + /// The AI (21) serial as a transmitter ID. Three shapes occur: the + /// serial is the ID exactly; the ID is followed by a non-alphanumeric + /// delimiter and trailing packaging data (seen on G6 transmitter boxes: + /// "-"); or the payload lost its FNC1 separators, in which + /// case the raw payload is scanned for the pattern. A 12-char + /// alphanumeric G7 serial never matches: the character after the + /// would-be ID is alphanumeric too. G5 rejection ("4…" prefix) is left + /// to the entry view, matching how a typed ID is handled. + public var transmitterIDCandidate: String? { + if let serial = serial, let id = Self.transmitterIDPrefix(of: serial) { + return id + } + guard isDexcomPackage else { return nil } + var rest = normalizedPayload[...] + while let range = rest.range(of: "21") { + if let id = Self.transmitterIDPrefix(of: String(rest[range.upperBound...])) { + return id + } + rest = rest[range.upperBound...] + } + return nil + } + + /// The AI (240) field as a sensor code. On G6 the applicator's Data + /// Matrix carries the 4-digit session code there (the field G7 uses + /// for its pairing code). It is the last element on applicator labels, + /// so an FNC1-stripped payload is recovered by reading the tail. + /// Applicator labels carry no GTIN, so unlike the transmitter ID this + /// is not gated on `isDexcomPackage`. Validity against the known-code + /// table is left to the entry view, matching a typed code. + public var sensorCodeCandidate: String? { + if let pin = value(forAI: "240"), pin.count == 4, pin.allSatisfy(\.isNumber) { + return pin + } + let tail = normalizedPayload.suffix(7) + guard tail.count == 7, + tail.hasPrefix("240"), + tail.dropFirst(3).allSatisfy(\.isNumber) + else { return nil } + return String(tail.dropFirst(3)) + } + + /// The leading 6 characters when they are alphanumeric and the string + /// either ends there or continues with a non-alphanumeric delimiter. + /// FNC1 does not count as a delimiter: a 6-char run followed by FNC1 is + /// a complete (21) field the strict parse handles, and in the fallback + /// scan it is the middle of a longer serial (a G7's 12-char serial + /// contains "21" six digits in), never a truncated transmitter ID. + private static func transmitterIDPrefix(of serial: String) -> String? { + guard serial.count >= 6 else { return nil } + let prefix = serial.prefix(6) + guard prefix.allSatisfy({ $0.isLetter || $0.isNumber }) else { return nil } + if serial.count == 6 { + return prefix.uppercased() + } + let next = serial.dropFirst(6).first! + guard !next.isLetter, !next.isNumber, next != GS1DataMatrixParser.fnc1 else { return nil } + return prefix.uppercased() + } +} diff --git a/Sources/G6SensorCore/Session/CommandQueue.swift b/Sources/G6SensorCore/Session/CommandQueue.swift new file mode 100644 index 0000000..dbf6c18 --- /dev/null +++ b/Sources/G6SensorCore/Session/CommandQueue.swift @@ -0,0 +1,74 @@ +// +// CommandQueue.swift +// G6SensorKit +// +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import Foundation + +/// A persisted queue of transmitter commands awaiting the next connection. +/// +/// Pure value logic, extracted from the G6 manager so the supersede and +/// stale-drop rules can be tested without LoopKit. The manager mirrors +/// `rawValues` into its persisted state so queued commands survive relaunch. +public struct CommandQueue: Equatable { + + public static let staleCalibrationInterval: TimeInterval = .minutes(5) + + public private(set) var commands: [Command] + + public init(_ commands: [Command] = []) { + self.commands = commands + } + + public init(rawValues: [Command.RawValue]) { + self.init(rawValues.compactMap(Command.init(rawValue:))) + } + + public var rawValues: [Command.RawValue] { + commands.map(\.rawValue) + } + + public var isEmpty: Bool { + commands.isEmpty + } + + /// Queues a command for the next connection cycle. Newest wins: any + /// queued command the new one supersedes is dropped first, so repeated + /// trips through setup cannot stack session starts. + /// + /// - Returns: the number of queued commands the new one replaced. + @discardableResult public mutating func enqueue(_ command: Command) -> Int { + let before = commands.count + commands.removeAll { $0.supersededBy(command) } + let superseded = before - commands.count + commands.append(command) + return superseded + } + + /// Pops the next command to send, silently dropping stale calibrations. + /// + /// - Returns: the command to send, plus any commands dropped as stale + /// (for logging). + public mutating func dequeue(now: Date = Date()) -> (next: Command?, dropped: [Command]) { + var dropped: [Command] = [] + var next: Command? + while let candidate = commands.first { + commands.removeFirst() + if case let .calibrateSensor(_, date) = candidate, + now.timeIntervalSince(date) > Self.staleCalibrationInterval + { + dropped.append(candidate) + continue + } + next = candidate + break + } + return (next, dropped) + } + + public mutating func removeAll() { + commands.removeAll() + } +} diff --git a/Sources/G6SensorCore/Session/TransmitterSession.swift b/Sources/G6SensorCore/Session/TransmitterSession.swift index 3176be4..17d77a9 100644 --- a/Sources/G6SensorCore/Session/TransmitterSession.swift +++ b/Sources/G6SensorCore/Session/TransmitterSession.swift @@ -466,10 +466,10 @@ public final class TransmitterSession: TransmitterConnectionDelegate { let ack = try peripheral.requestBackfill(startTime: startTime, endTime: endTime) - log.default("Backfill acknowledged: status %{public}@, backfillStatus %{public}@, id %{public}@, range %{public}@-%{public}@, length %{public}@, crc %{public}@", - String(ack.status), String(ack.backfillStatus), String(ack.identifier), - String(ack.startTime), String(ack.endTime), - String(ack.bufferLength), String(format: "%04x", ack.bufferCRC)) + log.default("Backfill acknowledged: %{public}@", + "status \(ack.status), backfillStatus \(ack.backfillStatus), id \(ack.identifier), " + + "range \(ack.startTime)-\(ack.endTime), length \(ack.bufferLength), " + + "crc \(String(format: "%04x", ack.bufferCRC))") // Nothing stored for the window: an empty result, not a failure. guard ack.bufferLength > 0 else { diff --git a/Sources/G6SensorCore/Support/Locked.swift b/Sources/G6SensorCore/Support/Locked.swift index fd6e35b..4a82830 100644 --- a/Sources/G6SensorCore/Support/Locked.swift +++ b/Sources/G6SensorCore/Support/Locked.swift @@ -31,6 +31,7 @@ internal class Locked { } } + @discardableResult func mutate(_ changes: (_ value: inout T) -> Void) -> T { os_unfair_lock_lock(&lock) defer { os_unfair_lock_unlock(&lock) } diff --git a/Sources/G6SensorCore/Transport/OSLog.swift b/Sources/G6SensorCore/Transport/OSLog.swift index 847d5be..999052d 100644 --- a/Sources/G6SensorCore/Transport/OSLog.swift +++ b/Sources/G6SensorCore/Transport/OSLog.swift @@ -34,7 +34,8 @@ extension OSLog { } private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) { - mirrorToFile(message, type: type, args) + let rendered = render(message, args) + mirrorToFile(rendered, type: type) switch args.count { case 0: @@ -50,7 +51,10 @@ extension OSLog { case 5: os_log(message, log: self, type: type, args[0], args[1], args[2], args[3], args[4]) default: - os_log(message, log: self, type: type, args) + // os_log can't take the array as varargs: handing it over reads + // garbage pointers for each %@ and crashes. Log the rendered line. + assertionFailure("OSLog supports at most 5 arguments, got \(args.count): \(message)") + os_log("%{public}@", log: self, type: type, rendered) } } @@ -65,15 +69,17 @@ extension OSLog { return Self.categories[ObjectIdentifier(self)] ?? "G6SensorKit" } - /// Mirrors every OSLog line into the exportable file log. Format - /// specifiers are OSLog-flavoured (%{public}@), so the privacy qualifier - /// is stripped before String(format:) sees them. - private func mirrorToFile(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) { + /// Format specifiers are OSLog-flavoured (%{public}@), so the privacy + /// qualifier is stripped before String(format:) sees them. + private func render(_ message: StaticString, _ args: [CVarArg]) -> String { let template = "\(message)" .replacingOccurrences(of: "%{public}", with: "%") .replacingOccurrences(of: "%{private}", with: "%") - let rendered = args.isEmpty ? template : String(format: template, arguments: args) + return args.isEmpty ? template : String(format: template, arguments: args) + } + /// Mirrors every OSLog line into the exportable file log. + private func mirrorToFile(_ rendered: String, type: OSLogType) { let level: String switch type { case .debug: level = "DEBUG" diff --git a/Sources/G6SensorCore/Transport/PeripheralManager.swift b/Sources/G6SensorCore/Transport/PeripheralManager.swift index 8239e10..834298b 100644 --- a/Sources/G6SensorCore/Transport/PeripheralManager.swift +++ b/Sources/G6SensorCore/Transport/PeripheralManager.swift @@ -147,7 +147,7 @@ extension PeripheralManager { } } - private func applyConfiguration(discoveryTimeout: TimeInterval = 2) throws { + private func applyConfiguration(discoveryTimeout: TimeInterval = 10) throws { try discoverServices(configuration.serviceCharacteristics.keys.map { $0 }, timeout: discoveryTimeout) for service in peripheral.services ?? [] { @@ -400,11 +400,13 @@ extension PeripheralManager: CBPeripheralDelegate { var notifyDelegate = false if let index = commandConditions.firstIndex(where: { (condition) -> Bool in - if case .valueUpdate(characteristic: characteristic, matching: let matching) = condition { - return matching?(characteristic.value) ?? true - } else { + guard case .valueUpdate(characteristic: let conditionCharacteristic, matching: let matching) = condition, + conditionCharacteristic == characteristic + else { return false } + + return matching?(characteristic.value) ?? true }) { commandConditions.remove(at: index) commandError = error @@ -414,7 +416,9 @@ extension PeripheralManager: CBPeripheralDelegate { } } else if let macro = configuration.valueUpdateMacros[characteristic.uuid] { macro(self) - } else if commandConditions.isEmpty { + } else { + // Unclaimed by any condition or macro: pass it along to the + // delegate even while an unrelated command is still pending. notifyDelegate = true // execute after the unlock } diff --git a/Tests/G6SensorCoreTests/CommandQueueTests.swift b/Tests/G6SensorCoreTests/CommandQueueTests.swift new file mode 100644 index 0000000..bdcb7cd --- /dev/null +++ b/Tests/G6SensorCoreTests/CommandQueueTests.swift @@ -0,0 +1,95 @@ +// +// CommandQueueTests.swift +// G6SensorKit +// +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import XCTest +@testable import G6SensorCore + +class CommandQueueTests: XCTestCase { + private let now = Date(timeIntervalSince1970: 1_700_000_000) + + func testEnqueueAppendsDistinctCommands() { + var queue = CommandQueue() + queue.enqueue(.startSensor(at: now, sensorCode: .none)) + queue.enqueue(.calibrateSensor(toMgDL: 120, at: now)) + + XCTAssertEqual(queue.commands.count, 2) + } + + func testEnqueueSupersedesSameKind() { + var queue = CommandQueue() + queue.enqueue(.startSensor(at: now, sensorCode: .none)) + queue.enqueue(.startSensor(at: now, sensorCode: .none)) + queue.enqueue(.calibrateSensor(toMgDL: 100, at: now)) + queue.enqueue(.calibrateSensor(toMgDL: 140, at: now)) + + XCTAssertEqual(queue.commands.count, 2) + guard case let .calibrateSensor(value, _) = queue.commands[1] else { + return XCTFail("expected calibration") + } + XCTAssertEqual(value, 140) + } + + func testEnqueueReturnsSupersededCount() { + var queue = CommandQueue() + queue.enqueue(.calibrateSensor(toMgDL: 100, at: now)) + queue.enqueue(.calibrateSensor(toMgDL: 110, at: now)) + // The 110 enqueue already superseded the 100 one, so a single queued + // calibration remains for the 120 to replace. + let superseded = queue.enqueue(.calibrateSensor(toMgDL: 120, at: now)) + XCTAssertEqual(superseded, 1) + XCTAssertEqual(queue.commands.count, 1) + } + + func testDequeueDropsStaleCalibrations() { + var queue = CommandQueue() + // 6 minutes old at dequeue time: beyond the 5-minute window. + queue.enqueue(.calibrateSensor(toMgDL: 120, at: now.addingTimeInterval(-360))) + queue.enqueue(.stopSensor(at: now)) + + let (next, dropped) = queue.dequeue(now: now) + + XCTAssertEqual(dropped.count, 1) + guard case .stopSensor? = next else { + return XCTFail("expected stopSensor, got \(String(describing: next))") + } + XCTAssertTrue(queue.isEmpty) + } + + func testDequeueKeepsFreshCalibration() { + var queue = CommandQueue() + queue.enqueue(.calibrateSensor(toMgDL: 120, at: now.addingTimeInterval(-60))) + + let (next, dropped) = queue.dequeue(now: now) + + XCTAssertTrue(dropped.isEmpty) + guard case .calibrateSensor? = next else { + return XCTFail("expected calibration, got \(String(describing: next))") + } + } + + func testDequeueEmptyReturnsNil() { + var queue = CommandQueue() + let (next, dropped) = queue.dequeue(now: now) + XCTAssertNil(next) + XCTAssertTrue(dropped.isEmpty) + } + + func testRawValuesRoundTrip() { + var queue = CommandQueue() + queue.enqueue(.startSensor(at: now, sensorCode: SensorCode("5931") ?? .none)) + queue.enqueue(.calibrateSensor(toMgDL: 123.4, at: now)) + queue.enqueue(.resetTransmitter) + + let restored = CommandQueue(rawValues: queue.rawValues) + XCTAssertEqual(restored, queue) + } + + func testRawValuesDropMalformedEntries() { + let queue = CommandQueue(rawValues: [["action": 99], [:]]) + XCTAssertTrue(queue.isEmpty) + } +} diff --git a/Tests/G6SensorCoreTests/G6TrendArrowTests.swift b/Tests/G6SensorCoreTests/G6TrendArrowTests.swift new file mode 100644 index 0000000..9062794 --- /dev/null +++ b/Tests/G6SensorCoreTests/G6TrendArrowTests.swift @@ -0,0 +1,53 @@ +// +// G6TrendArrowTests.swift +// G6SensorKit +// +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import XCTest +@testable import G6SensorCore + +/// Rates are integer tenths on the wire, so every case here is reachable. +class G6TrendArrowTests: XCTestCase { + func testNoRateHasNoArrow() { + XCTAssertNil(G6TrendArrow(dexcomRateMgDLPerMinute: nil)) + } + + func testArrowsByRate() { + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -4), .downDownDown) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -2.5), .downDown) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -1.5), .down) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 0), .flat) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 1.5), .up) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 2.5), .upUp) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 4), .upUpUp) + } + + /// Every threshold belongs to the steeper arrow, on both sides. + /// The old `..<(-3)` bucketing gave a single-down arrow at exactly -3.0. + func testThresholdsTakeTheSteeperArrow() { + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -3), .downDownDown) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 3), .upUpUp) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -2), .downDown) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 2), .upUp) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -1), .down) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 1), .up) + } + + /// Flat excludes both of its ends, so it is the narrowest bucket. + func testFlatIsOnlyBetweenTheUnitThresholds() { + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -0.9), .flat) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 0.9), .flat) + } + + /// Past +/-8 the official apps show no arrow rather than a triple one. + /// The wire carries Int8 tenths, so this reaches roughly +/-12.6. + func testImplausibleRatesHaveNoArrow() { + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: -8), .downDownDown) + XCTAssertEqual(G6TrendArrow(dexcomRateMgDLPerMinute: 8), .upUpUp) + XCTAssertNil(G6TrendArrow(dexcomRateMgDLPerMinute: -8.1)) + XCTAssertNil(G6TrendArrow(dexcomRateMgDLPerMinute: 8.1)) + XCTAssertNil(G6TrendArrow(dexcomRateMgDLPerMinute: 12.6)) + } +} diff --git a/Tests/G6SensorCoreTests/GS1DataMatrixParserTests.swift b/Tests/G6SensorCoreTests/GS1DataMatrixParserTests.swift new file mode 100644 index 0000000..c21b7d5 --- /dev/null +++ b/Tests/G6SensorCoreTests/GS1DataMatrixParserTests.swift @@ -0,0 +1,239 @@ +// +// GS1DataMatrixParserTests.swift +// G6SensorKit +// +// Ported from DexKit (DexKitTests/GS1DataMatrixParserTests.swift), trimmed +// to G6 alongside the parser. +// Copyright © 2026 Nightscout Foundation. MIT License. +// + +import XCTest +@testable import G6SensorCore + +/// Payloads follow the GS1 spec: a package Data Matrix = (01) GTIN, +/// (17) expiry, (10) lot, (21) serial, (240) additional product ID, with +/// FNC1 (\u{1D}) separating variable-length elements that aren't last. +final class GS1DataMatrixParserTests: XCTestCase { + private let fnc1 = "\u{1D}" + + // MARK: - Basic element extraction + + /// Real payload from a public G7 box image: (01) GTIN, (11) production + /// date, (17) expiry, (10) lot, (21) 12-char serial, (241) customer + /// part number. Note the box carries no (240): the pairing/sensor code + /// lives on the applicator label. + private var g7BoxPayload: String { + "01" + "00386270002839" + "11" + "230201" + "17" + "240731" + + "10" + "1523041787" + fnc1 + "21" + "448421319350" + fnc1 + "241" + "STP-AT-012301" + } + + func testParsesFullPayloadWithAIMPrefixAndFNC1() { + // ]d2 = AIM symbology identifier for GS1 Data Matrix + let elements = GS1DataMatrixParser.parse("]d2" + g7BoxPayload) + XCTAssertEqual(elements, [ + GS1Element(ai: "01", value: "00386270002839"), + GS1Element(ai: "11", value: "230201"), + GS1Element(ai: "17", value: "240731"), + GS1Element(ai: "10", value: "1523041787"), + GS1Element(ai: "21", value: "448421319350"), + GS1Element(ai: "241", value: "STP-AT-012301") + ]) + } + + func testParsesPayloadWithoutAIMPrefix() { + // VisionKit/Vision typically deliver the payload without "]d2". + let scan = G6PackageScan(payload: g7BoxPayload) + XCTAssertEqual(scan.gtin, "00386270002839") + XCTAssertEqual(scan.serial, "448421319350") + XCTAssertEqual(scan.value(forAI: "241"), "STP-AT-012301") + } + + func testStripsLeadingFNC1() { + // Some hardware scanners report the leading FNC1 of the first element. + let scan = G6PackageScan(payload: fnc1 + g7BoxPayload) + XCTAssertEqual(scan.gtin, "00386270002839") + XCTAssertEqual(scan.serial, "448421319350") + } + + // MARK: - Dexcom mapping + + func testG6PackageYieldsTransmitterIDCandidate() { + // G6 transmitter box: (21) serial is the 6-char transmitter ID. + let payload = "0100386270004070" + fnc1 + "218KAB12" + let scan = G6PackageScan(payload: payload) + + XCTAssertTrue(scan.isDexcomPackage) + XCTAssertEqual(scan.transmitterIDCandidate, "8KAB12") + } + + func testSensorPackageYieldsNoTransmitterIDCandidate() { + // G7 box (the public payload above): (21) is a 12-char sensor + // serial, not a transmitter ID. + let scan = G6PackageScan(payload: g7BoxPayload) + + XCTAssertTrue(scan.isDexcomPackage) + XCTAssertNil(scan.transmitterIDCandidate) + XCTAssertNil(scan.sensorCodeCandidate) // the box carries no (240) + } + + func testFNC1StrippedG7BoxYieldsNoCandidates() { + // The same public payload as scanners and pastes deliver it without + // separators: the dates still parse, nothing G6-usable falls out. + let payload = "0100386270002839112302011724073110152304178721448421319350241STP-AT-012301" + let scan = G6PackageScan(payload: payload) + + XCTAssertTrue(scan.isDexcomPackage) + XCTAssertEqual(scan.value(forAI: "11"), "230201") + XCTAssertEqual(scan.value(forAI: "17"), "240731") + XCTAssertNil(scan.transmitterIDCandidate) + XCTAssertNil(scan.sensorCodeCandidate) + } + + func testLowercaseSerialIsUppercased() { + let scan = G6PackageScan(payload: "0100386270004070" + fnc1 + "218kab12") + XCTAssertEqual(scan.transmitterIDCandidate, "8KAB12") + } + + func testNonDexcomGTINIsFlagged() { + // Another manufacturer's package: same GS1 shape, different company prefix. + let payload = "01" + "06991234567890" + fnc1 + "10LOT123" + fnc1 + "21991234ABCD5678" + let scan = G6PackageScan(payload: payload) + XCTAssertFalse(scan.isDexcomPackage) + XCTAssertEqual(scan.gtin, "06991234567890") + XCTAssertEqual(scan.lot, "LOT123") + XCTAssertEqual(scan.serial, "991234ABCD5678") + XCTAssertNil(scan.transmitterIDCandidate) + } + + // MARK: - G6 applicator sensor codes + + // The applicator payloads below are real, public G6 labels; the + // transmitter/sensor box structures mirror our own captures with + // serials, lots and dates anonymized. + + func testApplicatorYieldsSensorCodeCandidate() { + // Real public applicator labels: (10) 16-char lot glued to + // (240) + the 4-digit sensor code, FNC1 separators stripped. + let cases: [(payload: String, code: String)] = [ + ("10731863521434687D2405937", "5937"), + ("10527390421846477D2405931", "5931"), + ("10729148021365863F2409117", "9117") + ] + for (payload, code) in cases { + let scan = G6PackageScan(payload: payload) + XCTAssertEqual(scan.sensorCodeCandidate, code, payload) + XCTAssertNotNil(SensorCode(scan.sensorCodeCandidate), payload) + // No GTIN and no (21): an applicator label says nothing about the transmitter. + XCTAssertNil(scan.transmitterIDCandidate, payload) + XCTAssertFalse(scan.isDexcomPackage, payload) + } + } + + func testApplicatorWithFNC1YieldsSensorCodeCandidate() { + let payload = "10" + "731863521434687D" + fnc1 + "2405937" + let scan = G6PackageScan(payload: payload) + XCTAssertEqual(scan.lot, "731863521434687D") + XCTAssertEqual(scan.sensorCodeCandidate, "5937") + } + + func testApplicatorTailMustBeExactlyFourDigits() { + // "240" followed by 5 digits is not a sensor code. + let scan = G6PackageScan(payload: "10731863521434687D24059377") + XCTAssertNil(scan.sensorCodeCandidate) + } + + func testTransmitterBoxSerialWithTrailingPackagingData() { + // Transmitter box: (21) is the 6-char ID glued to "-". + let payload = "0100386270003072" + "241STT-GS-003" + fnc1 + + "10AB12CD34" + fnc1 + "218ZXY12-199270101" + let scan = G6PackageScan(payload: payload) + + XCTAssertTrue(scan.isDexcomPackage) + XCTAssertEqual(scan.transmitterIDCandidate, "8ZXY12") + XCTAssertNil(scan.sensorCodeCandidate) + } + + func testFNC1StrippedTransmitterBoxYieldsTransmitterID() { + // Same box with every FNC1 stripped: the strict parse cannot + // segment past (241), so the raw-payload fallback recovers the ID. + let payload = "0100386270003072241STT-GS-00310AB12CD34218ZXY12-199270101" + let scan = G6PackageScan(payload: payload) + + XCTAssertTrue(scan.isDexcomPackage) + XCTAssertEqual(scan.transmitterIDCandidate, "8ZXY12") + } + + func testTransmitterBoxSerialOnlyBarcode() { + // Real public G6 transmitter box: the small Data Matrix carries + // only (21) + the 6-char transmitter ID. + let scan = G6PackageScan(payload: "2188H03B") + + XCTAssertEqual(scan.transmitterIDCandidate, "88H03B") + XCTAssertNil(scan.sensorCodeCandidate) + XCTAssertFalse(scan.isDexcomPackage) + } + + func testTransmitterBoxFullDataMatrix() { + // Same box's large Data Matrix: (241) customer part number, (10) + // lot, (21) serial, (17) expiry. The GTIN is a separate linear + // barcode on the box, so no (01) appears here. + let payload = "241STT-OM-001" + fnc1 + "1018038762" + fnc1 + "2188H03B" + fnc1 + "17250226" + let scan = G6PackageScan(payload: payload) + + XCTAssertEqual(scan.elements, [ + GS1Element(ai: "241", value: "STT-OM-001"), + GS1Element(ai: "10", value: "18038762"), + GS1Element(ai: "21", value: "88H03B"), + GS1Element(ai: "17", value: "250226") + ]) + XCTAssertEqual(scan.transmitterIDCandidate, "88H03B") + XCTAssertNil(scan.sensorCodeCandidate) + XCTAssertFalse(scan.isDexcomPackage) + } + + func testSensorBoxYieldsNoCandidates() { + // Sensor box: GTIN, customer part number, dates; no code at all. + // The sensor code is only on the applicator label. + let payload = "0100386270000927" + "2419500-46107380885" + "11251207" + "17270630" + let scan = G6PackageScan(payload: payload) + + XCTAssertTrue(scan.isDexcomPackage) + XCTAssertNil(scan.transmitterIDCandidate) + XCTAssertNil(scan.sensorCodeCandidate) + } + + // MARK: - Robustness + + func testVariableLengthSerialAtEndOfPayloadWithoutFNC1() { + let payload = "0100386270004758" + fnc1 + "21ABCD12345678" + XCTAssertEqual(GS1DataMatrixParser.parse(payload).last, + GS1Element(ai: "21", value: "ABCD12345678")) + } + + func testStopsAtUnknownAIKeepingParsedPrefix() { + // (99) is not in the registry; parsing stops but keeps (01) and (21). + let payload = "0100386270004758" + fnc1 + "218KAB12" + fnc1 + "99JUNK" + let elements = GS1DataMatrixParser.parse(payload) + XCTAssertEqual(elements, [ + GS1Element(ai: "01", value: "00386270004758"), + GS1Element(ai: "21", value: "8KAB12") + ]) + } + + func testTruncatedFixedLengthFieldStopsParsing() { + let payload = "0100386" // truncated GTIN + XCTAssertTrue(GS1DataMatrixParser.parse(payload).isEmpty) + } + + func testGarbageYieldsNoElements() { + XCTAssertTrue(GS1DataMatrixParser.parse("hello world").isEmpty) + XCTAssertTrue(GS1DataMatrixParser.parse("").isEmpty) + } + + func testFourDigitAIIsNotMisreadAsTwoDigitAI() { + // "240..." must read as AI 240, not AI 24 + garbage. + let payload = "0100386270004758" + fnc1 + "2401249" + XCTAssertEqual(GS1DataMatrixParser.parse(payload).last, + GS1Element(ai: "240", value: "1249")) + } +} diff --git a/Tests/G6SensorCoreTests/TransmitterIDTests.swift b/Tests/G6SensorCoreTests/TransmitterIDTests.swift index 59aedba..c807053 100644 --- a/Tests/G6SensorCoreTests/TransmitterIDTests.swift +++ b/Tests/G6SensorCoreTests/TransmitterIDTests.swift @@ -16,5 +16,18 @@ class TransmitterIDTests: XCTestCase { XCTAssertEqual("e60d4a7999b0fbb2", id.computeHash(of: Data(hexadecimalString: "0123456789abcdef")!)!.hexadecimalString) } - + + /// The prefix decides the model: 8… is G6, 5… and C… are Dexcom ONE. + func testModelByPrefix() { + XCTAssertEqual(TransmitterID(id: "8ZXY12").model, .g6) + XCTAssertEqual(TransmitterID(id: "5ZXY12").model, .one) + XCTAssertEqual(TransmitterID(id: "CZXY12").model, .one) + XCTAssertEqual(TransmitterID(id: "cZXY12").model, .one) + } + + /// An unrecognized prefix is the stock case, not a failure. + func testUnknownPrefixFallsBackToG6() { + XCTAssertEqual(TransmitterID(id: "9ZXY12").model, .g6) + } + }