diff --git a/.gitignore b/.gitignore
index 95e915d310..1195ca769c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,7 +50,10 @@ Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
-Carthage/Checkouts/
+.gitmodules
+.gitignore
+Carthage/Checkouts/*
+Carthage/Build/
Carthage/Build/tvOS/
Carthage/Build/iOS/*.bcsymbolmap
Carthage/Build/iOS/*.dSYM
@@ -74,3 +77,6 @@ RemoteSettings.plist
# Framework development
Loop.xcworkspace
+
+# Automatically updated during build phase.
+Loop/GitInfo.plist
diff --git a/Cartfile b/Cartfile
index 4f8e32a7af..389451e14c 100644
--- a/Cartfile
+++ b/Cartfile
@@ -1,7 +1,7 @@
-github "LoopKit/LoopKit" == 1.5.6
-github "LoopKit/CGMBLEKit" == 2.1
-github "i-schuetz/SwiftCharts" == 0.6.1
-github "mddub/dexcom-share-client-swift" == 0.4.1
-github "mddub/G4ShareSpy" == 0.3.3
-github "ps2/rileylink_ios" == 2.0.0
-github "LoopKit/Amplitude-iOS" "decreepify"
+github "erikdi/LoopKit" == 1.5.6
+github "erikdi/CGMBLEKit" "v2.1.1backport2"
+github "erikdi/SwiftCharts" == 0.6.2
+github "erikdi/dexcom-share-client-swift" == 0.4.1
+github "erikdi/G4ShareSpy" == 0.3.3
+github "erikdi/rileylink_ios" "v2.0.2backport1"
+github "erikdi/Amplitude-iOS" "decreepify"
diff --git a/Cartfile.resolved b/Cartfile.resolved
index 5b46c41bb2..dcd9e0c287 100644
--- a/Cartfile.resolved
+++ b/Cartfile.resolved
@@ -1,7 +1,7 @@
-github "LoopKit/Amplitude-iOS" "2137d5fd44bf630ed33e1e72d7af6d8f8612f270"
-github "LoopKit/CGMBLEKit" "v2.1.0"
-github "LoopKit/LoopKit" "v1.5.6"
-github "i-schuetz/SwiftCharts" "6b55a26a7b0b95e49202ddc1db5404702fce114f"
-github "mddub/G4ShareSpy" "v0.3.3"
-github "mddub/dexcom-share-client-swift" "v0.4.1"
-github "ps2/rileylink_ios" "v2.0.0"
+github "erikdi/Amplitude-iOS" "2137d5fd44bf630ed33e1e72d7af6d8f8612f270"
+github "erikdi/CGMBLEKit" "v2.1.1backport2"
+github "erikdi/G4ShareSpy" "v0.3.3"
+github "erikdi/LoopKit" "v1.5.6"
+github "erikdi/SwiftCharts" "0.6.2"
+github "erikdi/dexcom-share-client-swift" "v0.4.1"
+github "erikdi/rileylink_ios" "v2.0.2backport1"
diff --git a/Carthage/Build/.Amplitude-iOS.version b/Carthage/Build/.Amplitude-iOS.version
index 6c790cc4b2..e13cee39bb 100644
--- a/Carthage/Build/.Amplitude-iOS.version
+++ b/Carthage/Build/.Amplitude-iOS.version
@@ -12,7 +12,7 @@
"iOS" : [
{
"name" : "Amplitude",
- "hash" : "5f7cea951cf973d78073e75b87cd222891b181a3879b081a42f057d983b7c389"
+ "hash" : "b639090c9e797c968d7393300f374f740df06212b481538462683c19ef57cede"
}
]
}
\ No newline at end of file
diff --git a/Carthage/Build/.CGMBLEKit.version b/Carthage/Build/.CGMBLEKit.version
index 0e5d1875f6..7bb76ab503 100644
--- a/Carthage/Build/.CGMBLEKit.version
+++ b/Carthage/Build/.CGMBLEKit.version
@@ -5,17 +5,17 @@
"watchOS" : [
{
"name" : "CGMBLEKit",
- "hash" : "ade5de40f0722c07387d73c66678eefb12db2e028e0eb3ced4810ab415469e16"
+ "hash" : "f43b2721a52489544e9b8841f1b5259ca992ec02eb6b5d1fc43b15cb8ba05def"
}
],
"tvOS" : [
],
- "commitish" : "v2.1.0",
+ "commitish" : "v2.1.1backport2",
"iOS" : [
{
"name" : "CGMBLEKit",
- "hash" : "48dd66b1f45d9e92de8553d4e9f24cb9fb1ff2b1fdf7be0d2b8fda10d1ec8ea7"
+ "hash" : "1a5ae655cb75722ecaa6fa662c1a431dd19628b236ae3b3c0a5f0450f47232b9"
}
]
}
\ No newline at end of file
diff --git a/Carthage/Build/.G4ShareSpy.version b/Carthage/Build/.G4ShareSpy.version
index 5f2d85a2a6..f3cb9b5dad 100644
--- a/Carthage/Build/.G4ShareSpy.version
+++ b/Carthage/Build/.G4ShareSpy.version
@@ -12,7 +12,7 @@
"iOS" : [
{
"name" : "G4ShareSpy",
- "hash" : "058f1119e227d680500b4adec40a5263cf61104be2580549218f6b5876edc20f"
+ "hash" : "a398e5bc2917ff5113351f70712f463a2a7427a745c29a356de288dbe73397fa"
}
]
}
\ No newline at end of file
diff --git a/Carthage/Build/.LoopKit.version b/Carthage/Build/.LoopKit.version
index 6777a46027..80e86a0117 100644
--- a/Carthage/Build/.LoopKit.version
+++ b/Carthage/Build/.LoopKit.version
@@ -12,19 +12,19 @@
"iOS" : [
{
"name" : "GlucoseKit",
- "hash" : "1ef4ccb9582a5a31643443c4e5c8d0fef7ef760c30cd09f545508e6a8458ae92"
+ "hash" : "b487fcaef21538f2ef2ccb289d8ba7a78efa9477e66decf23c7d6cf4ef6d45b0"
},
{
- "name" : "InsulinKit",
- "hash" : "15de73844cdbc5e2e1277ab01ac5d39c8085f5aeb6718aab4a8257c1943c902e"
+ "name" : "CarbKit",
+ "hash" : "c68c07dde9d0eb0b55ea3d20494230e66bc6572eedc0339ed8d4e90438b9beec"
},
{
- "name" : "LoopKit",
- "hash" : "4fc0c5f661d96f375b30647728c1a2ff2164cd41ce046bbb86f8dc86bf9b76a8"
+ "name" : "InsulinKit",
+ "hash" : "4a1462c988a7092b1b185955477b6877dea675e0a4ffdc7a50beeab680303a19"
},
{
- "name" : "CarbKit",
- "hash" : "aed7066da389d043482db40034f6d63b220b15852d0f6fe7ef9739ee8835b87f"
+ "name" : "LoopKit",
+ "hash" : "248dc8b7159ee695fce25877fe0d46d3fecad26403f02c3b26848f06a8efe447"
}
]
}
\ No newline at end of file
diff --git a/Carthage/Build/.SwiftCharts.version b/Carthage/Build/.SwiftCharts.version
index b050a4083a..e526c0f877 100644
--- a/Carthage/Build/.SwiftCharts.version
+++ b/Carthage/Build/.SwiftCharts.version
@@ -8,14 +8,14 @@
"tvOS" : [
{
"name" : "SwiftCharts",
- "hash" : "89105ffbd390eb5cad0e5a73421b7a6e85064b3b0d9f78153b64261cfb932425"
+ "hash" : "3e8807a9b894da18c11dbfa0a30ac98799dafa05a19f1c84e80fdc3afbde3a9a"
}
],
- "commitish" : "6b55a26a7b0b95e49202ddc1db5404702fce114f",
+ "commitish" : "0.6.2",
"iOS" : [
{
"name" : "SwiftCharts",
- "hash" : "09c973532129d22bb0757f475ed90e0e207e00b80d02e1ec01d6291fa6564636"
+ "hash" : "21743d2c153fc2bf15ecf1e11f6843fa3e1c543df0f68576e7593430890865d0"
}
]
}
\ No newline at end of file
diff --git a/Carthage/Build/.dexcom-share-client-swift.version b/Carthage/Build/.dexcom-share-client-swift.version
index bff096e2f7..7b2b90b9b6 100644
--- a/Carthage/Build/.dexcom-share-client-swift.version
+++ b/Carthage/Build/.dexcom-share-client-swift.version
@@ -12,7 +12,7 @@
"iOS" : [
{
"name" : "ShareClient",
- "hash" : "1c64d1e57e896d8de9a262e02b58547e8a0b966090a4886c9dcbf34554dbdd7b"
+ "hash" : "e12befbafd514d41791fb3010535c2fbf875dc5974fa2446bf7cb5da281b9b91"
}
]
}
\ No newline at end of file
diff --git a/Carthage/Build/.rileylink_ios.version b/Carthage/Build/.rileylink_ios.version
index 61db1ce0fe..9889f0c591 100644
--- a/Carthage/Build/.rileylink_ios.version
+++ b/Carthage/Build/.rileylink_ios.version
@@ -5,37 +5,37 @@
"watchOS" : [
{
"name" : "RileyLinkBLEKit",
- "hash" : "c94d79d1403870df4539dd531024d5b44333642f28d49f8e9f4e26fbad42fd7a"
+ "hash" : "43073cf0c719a84eb4d41c99219271da50661281c280d5d7ad092beaf057d686"
}
],
"tvOS" : [
],
- "commitish" : "v2.0.0",
+ "commitish" : "v2.0.2backport1",
"iOS" : [
{
"name" : "Crypto",
- "hash" : "03768a89eb61f55cc386ae4586048ae851d61373da6868aab905db2a2586efc7"
+ "hash" : "5599739f53d727bb21d987a90be2d73e760d8e7b5af41801159c11696a782335"
},
{
"name" : "RileyLinkKitUI",
- "hash" : "dc1035fdadd445d12f383f01c10225b62aa7eca1f4da9016420d1835487628bb"
+ "hash" : "66b7d38bd232e4590134d10db4df2b38473d2a956890657666fcaf42de969233"
},
{
"name" : "RileyLinkKit",
- "hash" : "69fd21c5cd31ca8fe4611b01139780b1df32e71400a6fd91ebc865d85791b0a5"
+ "hash" : "de8034341ce3d14de9fc715cc6ddc430465d1358a6bf34e6f68bc30ee54eece0"
},
{
"name" : "NightscoutUploadKit",
- "hash" : "d3fe0bc023a0a275dddec6bba616ff38239acf44ee71879e270a3f8690e4961d"
+ "hash" : "b28868e34ddf6816ce6103fc5bf577b8638c0ba7e0d351a909e06fbfcef4a301"
},
{
"name" : "RileyLinkBLEKit",
- "hash" : "a95573627d6ad9a853c620938e53e97d4c054553fde4af097a489038b020fd2f"
+ "hash" : "0431a7b4fee0496e176b2f587bd39c7bcba7b039264301e050a77ada5f2fdaf2"
},
{
"name" : "MinimedKit",
- "hash" : "c5f8ad60aea1a15646ed78afcb485435b55cb6e5b54d7d299d2415e5fa74cf38"
+ "hash" : "a014ab1aa4eb9da3169a8a42f5bcc14865842de3b90002b1c04bd533d6151ce0"
}
]
}
\ No newline at end of file
diff --git a/Carthage/Build/iOS/Amplitude.framework/Amplitude b/Carthage/Build/iOS/Amplitude.framework/Amplitude
index a761b896f9..c3aa827a79 100755
Binary files a/Carthage/Build/iOS/Amplitude.framework/Amplitude and b/Carthage/Build/iOS/Amplitude.framework/Amplitude differ
diff --git a/Carthage/Build/iOS/Amplitude.framework/Info.plist b/Carthage/Build/iOS/Amplitude.framework/Info.plist
index a963fa3eb3..c8749e1025 100644
Binary files a/Carthage/Build/iOS/Amplitude.framework/Info.plist and b/Carthage/Build/iOS/Amplitude.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/CGMBLEKit.framework/CGMBLEKit b/Carthage/Build/iOS/CGMBLEKit.framework/CGMBLEKit
index a158773f4f..ca8c43efd1 100755
Binary files a/Carthage/Build/iOS/CGMBLEKit.framework/CGMBLEKit and b/Carthage/Build/iOS/CGMBLEKit.framework/CGMBLEKit differ
diff --git a/Carthage/Build/iOS/CGMBLEKit.framework/Headers/CGMBLEKit-Swift.h b/Carthage/Build/iOS/CGMBLEKit.framework/Headers/CGMBLEKit-Swift.h
index 7c90b3f2f3..25706e8fa4 100644
--- a/Carthage/Build/iOS/CGMBLEKit.framework/Headers/CGMBLEKit-Swift.h
+++ b/Carthage/Build/iOS/CGMBLEKit.framework/Headers/CGMBLEKit-Swift.h
@@ -226,6 +226,11 @@ SWIFT_CLASS("_TtC9CGMBLEKit17PeripheralManager")
@end
+@interface PeripheralManager (SWIFT_EXTENSION(CGMBLEKit))
+@property (nonatomic, readonly, copy) NSString * _Nonnull debugDescription;
+@end
+
+
diff --git a/Carthage/Build/iOS/CGMBLEKit.framework/Info.plist b/Carthage/Build/iOS/CGMBLEKit.framework/Info.plist
index 4b5b588798..8a04e6d4db 100644
Binary files a/Carthage/Build/iOS/CGMBLEKit.framework/Info.plist and b/Carthage/Build/iOS/CGMBLEKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm.swiftmodule
index d3a314d97a..cbb7061fa2 100644
Binary files a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm64.swiftmodule
index 458a8c6327..6ed2ffc7e4 100644
Binary files a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/i386.swiftmodule
index f77faf3bac..e229ce60f6 100644
Binary files a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/x86_64.swiftmodule
index 8da9a201f2..a6a813884d 100644
Binary files a/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/CGMBLEKit.framework/Modules/CGMBLEKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Assets.car b/Carthage/Build/iOS/CarbKit.framework/Assets.car
index 9a30a8fa07..c03ab63409 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Assets.car and b/Carthage/Build/iOS/CarbKit.framework/Assets.car differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit b/Carthage/Build/iOS/CarbKit.framework/CarbKit
index fec280e936..989974a17d 100755
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit and b/Carthage/Build/iOS/CarbKit.framework/CarbKit differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/objects-11.0+.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/objects-11.0+.nib
index 0486bfe4ee..5964b359aa 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/objects-11.0+.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/objects-11.0+.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/runtime.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/runtime.nib
index cfa3f7f0e0..ca82c96b11 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/runtime.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib/runtime.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/objects-11.0+.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/objects-11.0+.nib
index 377d06b04d..83310fe145 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/objects-11.0+.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/objects-11.0+.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/runtime.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/runtime.nib
index fa4f7c7d8e..4ee10aa22a 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/runtime.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib/runtime.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/objects-11.0+.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/objects-11.0+.nib
index c5732723bd..81f4583ed3 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/objects-11.0+.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/objects-11.0+.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/runtime.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/runtime.nib
index 2fa8dbae67..f81f8ae77a 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/runtime.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib/runtime.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/objects-11.0+.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/objects-11.0+.nib
index 1ca760e93e..6fc782e4e7 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/objects-11.0+.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/objects-11.0+.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/runtime.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/runtime.nib
index e45e7b27f3..269ebdc043 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/runtime.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib/runtime.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/objects-11.0+.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/objects-11.0+.nib
index c3f36895b5..dd2110d6ba 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/objects-11.0+.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/objects-11.0+.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/runtime.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/runtime.nib
index 8611f33726..ad48f2458f 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/runtime.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib/runtime.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/objects-11.0+.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/objects-11.0+.nib
index bb0d54205d..5d1992eb71 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/objects-11.0+.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/objects-11.0+.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/runtime.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/runtime.nib
index 4e6d64c350..6c5cf4bb6c 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/runtime.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib/runtime.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/objects-11.0+.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/objects-11.0+.nib
index c43b9bdc4f..6f5241cca5 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/objects-11.0+.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/objects-11.0+.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/runtime.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/runtime.nib
index 9952806b2b..403d771880 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/runtime.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib/runtime.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/DatePickerTableViewCell.nib b/Carthage/Build/iOS/CarbKit.framework/DatePickerTableViewCell.nib
index 02002de103..97214c4099 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/DatePickerTableViewCell.nib and b/Carthage/Build/iOS/CarbKit.framework/DatePickerTableViewCell.nib differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h b/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h
index f64955a0e9..58e685e56f 100644
--- a/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h
+++ b/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/CarbKit.framework/Info.plist b/Carthage/Build/iOS/CarbKit.framework/Info.plist
index 081e65c882..9556cda2c5 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Info.plist and b/Carthage/Build/iOS/CarbKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc
index f280575a1e..701363d28f 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule
index cb9e02b1b2..8fcb8dda71 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc
index 0afc32a22d..c5add95c7c 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule
index 3bae421e9a..7b538286b1 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc
index 2625653fae..1d8fbe204b 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule
index 3bb540a1c9..2f5afd461b 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc
index 21a4c45026..1feeeb4a5f 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule
index 5460cbc3e1..24b4d49c07 100644
Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/Crypto.framework/Crypto b/Carthage/Build/iOS/Crypto.framework/Crypto
index 3e1d7d6d30..93271d31d0 100755
Binary files a/Carthage/Build/iOS/Crypto.framework/Crypto and b/Carthage/Build/iOS/Crypto.framework/Crypto differ
diff --git a/Carthage/Build/iOS/Crypto.framework/Info.plist b/Carthage/Build/iOS/Crypto.framework/Info.plist
index 966cd811ab..44e8837657 100644
Binary files a/Carthage/Build/iOS/Crypto.framework/Info.plist and b/Carthage/Build/iOS/Crypto.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy b/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy
index 5b142ba3ce..deefc01dda 100755
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy and b/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Headers/G4ShareSpy-Swift.h b/Carthage/Build/iOS/G4ShareSpy.framework/Headers/G4ShareSpy-Swift.h
index 644391d358..150e91ce9a 100644
--- a/Carthage/Build/iOS/G4ShareSpy.framework/Headers/G4ShareSpy-Swift.h
+++ b/Carthage/Build/iOS/G4ShareSpy.framework/Headers/G4ShareSpy-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist b/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist
index e8865484d7..22a5cb5280 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist and b/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftdoc
index 9de305ac1e..5c0a962d7c 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftmodule
index d532a307ad..f49287599f 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftdoc
index e01f4aa77a..2e6f5c8d46 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftmodule
index 8926c63cd6..e163489ccb 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftdoc
index 3e96c1ea3d..e2bc37f5dc 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftmodule
index 450b90ff49..9f0c3e7312 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftdoc
index 6c19f2d975..693e795a7b 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftmodule
index 67c4ce02f8..902c90510b 100644
Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/G4ShareSpy.framework/Modules/G4ShareSpy.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit b/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit
index 89ee4e02d4..304a232a88 100755
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit and b/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h b/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h
index 29e2d646ee..80879e9cea 100644
--- a/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h
+++ b/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Info.plist b/Carthage/Build/iOS/GlucoseKit.framework/Info.plist
index 816d699ec2..383ca63720 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Info.plist and b/Carthage/Build/iOS/GlucoseKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc
index 912099f682..c889a22deb 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule
index d45c871351..12ed503ba8 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc
index 3fa79dd5bc..6c6442bc32 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule
index 5bfc73a677..9bbd3406d1 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc
index 0d21ad5e50..a202c9dc36 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule
index 67356f2a5c..305e0fc4a3 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc
index aa1836f17c..6cbbfb3c78 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule
index fd10dc28a6..43d7770b24 100644
Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib
index f5ac10e31e..534039d296 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib and b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib
index 73ab9d38c7..9489c29675 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib and b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Headers/InsulinKit-Swift.h b/Carthage/Build/iOS/InsulinKit.framework/Headers/InsulinKit-Swift.h
index cd3119532f..237233111f 100644
--- a/Carthage/Build/iOS/InsulinKit.framework/Headers/InsulinKit-Swift.h
+++ b/Carthage/Build/iOS/InsulinKit.framework/Headers/InsulinKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Info.plist b/Carthage/Build/iOS/InsulinKit.framework/Info.plist
index 8994736d76..1fb758e93d 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Info.plist and b/Carthage/Build/iOS/InsulinKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/InsulinKit b/Carthage/Build/iOS/InsulinKit.framework/InsulinKit
index 50b0bc1304..5dc43b2c71 100755
Binary files a/Carthage/Build/iOS/InsulinKit.framework/InsulinKit and b/Carthage/Build/iOS/InsulinKit.framework/InsulinKit differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc
index e861493884..0a17b0a500 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule
index 8c5f8d49d6..7d545f24bc 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc
index 09332307f6..c252f86d4a 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule
index 9d01099fe5..37a864909a 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc
index 6e5f5ee129..46f72431ac 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule
index eaafed5888..44b10c4cf3 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc
index d5c39cd5a9..a8d8ac3289 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule
index 7de63982a3..a6efb1c2cc 100644
Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Assets.car b/Carthage/Build/iOS/LoopKit.framework/Assets.car
index 19ab224fb1..fceda759ed 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Assets.car and b/Carthage/Build/iOS/LoopKit.framework/Assets.car differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib
index e2ca39c33b..3375514140 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib
index fdbef1acfb..615e9ece6b 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Headers/LoopKit-Swift.h b/Carthage/Build/iOS/LoopKit.framework/Headers/LoopKit-Swift.h
index 605b19b620..5189892591 100644
--- a/Carthage/Build/iOS/LoopKit.framework/Headers/LoopKit-Swift.h
+++ b/Carthage/Build/iOS/LoopKit.framework/Headers/LoopKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/LoopKit.framework/Info.plist b/Carthage/Build/iOS/LoopKit.framework/Info.plist
index 3e0a932f94..9e84d97afd 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Info.plist and b/Carthage/Build/iOS/LoopKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/LoopKit b/Carthage/Build/iOS/LoopKit.framework/LoopKit
index 220f81f8e0..6e94f51436 100755
Binary files a/Carthage/Build/iOS/LoopKit.framework/LoopKit and b/Carthage/Build/iOS/LoopKit.framework/LoopKit differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc
index 6bd298961d..8d654a12e9 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule
index c96ec396fe..c6aed2e2a8 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc
index c1b17ee3d7..792fff8b57 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule
index db3dca3c5c..149aa5b3c7 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc
index b2c27d5031..896f635cf5 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule
index 83fd0068ae..3046d5b82f 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc
index b9c7e5d8eb..fd3aa5a25f 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule
index db50dbbd0b..6949f5a777 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib
index 20fa2a7277..c4aa83d04f 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib differ
diff --git a/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib
index 18cfa641fa..22213d69cb 100644
Binary files a/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Headers/MinimedKit-Swift.h b/Carthage/Build/iOS/MinimedKit.framework/Headers/MinimedKit-Swift.h
index 93606bd677..382b31c857 100644
--- a/Carthage/Build/iOS/MinimedKit.framework/Headers/MinimedKit-Swift.h
+++ b/Carthage/Build/iOS/MinimedKit.framework/Headers/MinimedKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Info.plist b/Carthage/Build/iOS/MinimedKit.framework/Info.plist
index 92cc757b5e..ca1953cd1e 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Info.plist and b/Carthage/Build/iOS/MinimedKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/MinimedKit b/Carthage/Build/iOS/MinimedKit.framework/MinimedKit
index d84e493e3d..81c1907c51 100755
Binary files a/Carthage/Build/iOS/MinimedKit.framework/MinimedKit and b/Carthage/Build/iOS/MinimedKit.framework/MinimedKit differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftdoc
index b564fce1c8..9ebd09f00b 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule
index f2b3b0e273..2fd37573c7 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftdoc
index ce6bf9551b..ebf2de9618 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule
index 0f180096a7..4002b615be 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftdoc
index 2bdc4adc89..55237cf27a 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule
index 8668c50051..1bde938f99 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftdoc
index eea7806599..18cc14b599 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule
index a5a2164939..514bf53920 100644
Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Headers/NightscoutUploadKit-Swift.h b/Carthage/Build/iOS/NightscoutUploadKit.framework/Headers/NightscoutUploadKit-Swift.h
index 5bc4b7be48..80c8296e48 100644
--- a/Carthage/Build/iOS/NightscoutUploadKit.framework/Headers/NightscoutUploadKit-Swift.h
+++ b/Carthage/Build/iOS/NightscoutUploadKit.framework/Headers/NightscoutUploadKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist b/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist
index 05bdf8efd4..24596d8d10 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc
index f9ad01ab02..47bf27b590 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule
index 3a9353945e..a27bf55f1e 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc
index 1dcfd3bfc8..45cfdcdd03 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule
index 049671c9cc..4742ee67af 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc
index 58b739fff0..9dee09c2aa 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule
index 80f52949c4..1a08190d27 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc
index 11f0095f4f..edbf42caad 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule
index 46745e245f..e5bc128931 100644
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit b/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit
index bfdfcfd5ab..346eaa43c6 100755
Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit and b/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEKit-Swift.h b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEKit-Swift.h
index f291e9aff6..2626e671ee 100644
--- a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEKit-Swift.h
+++ b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist
index 607b825d56..1dd8a46734 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftdoc
index cbe667511c..b1a2af4ced 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftmodule
index 9ff1531c58..9bfb0b316e 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftdoc
index efa74fdffd..bb351bf434 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftmodule
index cba3abdfd0..718dad0476 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftdoc
index b704f724d7..9b998ed996 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftmodule
index b987761709..57c30e85df 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftdoc
index 899270802a..f6aad0b57c 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftmodule
index bb08da24ee..61e3050ae9 100644
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Modules/RileyLinkBLEKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit b/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit
index ed739b996e..174ddfc2ac 100755
Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Headers/RileyLinkKit-Swift.h b/Carthage/Build/iOS/RileyLinkKit.framework/Headers/RileyLinkKit-Swift.h
index f204a949f5..db76fd479a 100644
--- a/Carthage/Build/iOS/RileyLinkKit.framework/Headers/RileyLinkKit-Swift.h
+++ b/Carthage/Build/iOS/RileyLinkKit.framework/Headers/RileyLinkKit-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist b/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist
index 7939db8efa..538a4b8f9d 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist and b/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc
index b951c256fc..e52ced9e2e 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule
index 069818c359..1a5fbb0e0e 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc
index 206ae7c5a1..3808ca7ee3 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule
index acf856abfd..8b6a568713 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc
index a9287c28bb..6ffc00e5df 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule
index a636df0bf7..5ef661afdc 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc
index c891fbf837..f8f5e3f808 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule
index 64c21e39d7..63f86112c4 100644
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit b/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit
index 1e6f66fe23..f2373021ad 100755
Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit and b/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Headers/RileyLinkKitUI-Swift.h b/Carthage/Build/iOS/RileyLinkKitUI.framework/Headers/RileyLinkKitUI-Swift.h
index 1bad713eda..b481be9b02 100644
--- a/Carthage/Build/iOS/RileyLinkKitUI.framework/Headers/RileyLinkKitUI-Swift.h
+++ b/Carthage/Build/iOS/RileyLinkKitUI.framework/Headers/RileyLinkKitUI-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Info.plist b/Carthage/Build/iOS/RileyLinkKitUI.framework/Info.plist
index 277931a2e6..eb4ad1eccf 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Info.plist and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftdoc
index ff1e4cdd71..ce543c4bd5 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftmodule
index a6529ab2b2..0fa5c5f2bd 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftdoc
index 4082c8ca6c..73452bbc0b 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftmodule
index fe50a6db5b..6251a3f11c 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftdoc
index 6a900caf68..bf98083d32 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftmodule
index d70d4a8750..9d0f267990 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftdoc
index 9347052a5b..8c7d64163d 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftmodule
index 72dbb65486..7db34a4f9c 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/RileyLinkKitUI.framework/Modules/RileyLinkKitUI.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/RileyLinkKitUI b/Carthage/Build/iOS/RileyLinkKitUI.framework/RileyLinkKitUI
index e535b2c3da..e688a5e5d8 100755
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/RileyLinkKitUI and b/Carthage/Build/iOS/RileyLinkKitUI.framework/RileyLinkKitUI differ
diff --git a/Carthage/Build/iOS/RileyLinkKitUI.framework/TextFieldTableViewCell.nib b/Carthage/Build/iOS/RileyLinkKitUI.framework/TextFieldTableViewCell.nib
index ce1a4b7986..60b1cbbad6 100644
Binary files a/Carthage/Build/iOS/RileyLinkKitUI.framework/TextFieldTableViewCell.nib and b/Carthage/Build/iOS/RileyLinkKitUI.framework/TextFieldTableViewCell.nib differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Headers/ShareClient-Swift.h b/Carthage/Build/iOS/ShareClient.framework/Headers/ShareClient-Swift.h
index 7f870ce126..1f8045aa90 100644
--- a/Carthage/Build/iOS/ShareClient.framework/Headers/ShareClient-Swift.h
+++ b/Carthage/Build/iOS/ShareClient.framework/Headers/ShareClient-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
diff --git a/Carthage/Build/iOS/ShareClient.framework/Info.plist b/Carthage/Build/iOS/ShareClient.framework/Info.plist
index ecc5020733..8f72ea3922 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Info.plist and b/Carthage/Build/iOS/ShareClient.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftdoc
index 9de305ac1e..5c0a962d7c 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule
index 329f119152..ef7201a058 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftdoc
index e01f4aa77a..2e6f5c8d46 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule
index ce0eb8c131..f5d0660b58 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftdoc
index 3e96c1ea3d..e2bc37f5dc 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule
index 35c27b7106..aef319349a 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftdoc
index 6c19f2d975..693e795a7b 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule
index 3141130a9d..fcf1ce9906 100644
Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/ShareClient.framework/ShareClient b/Carthage/Build/iOS/ShareClient.framework/ShareClient
index af4dfba70f..781b72a611 100755
Binary files a/Carthage/Build/iOS/ShareClient.framework/ShareClient and b/Carthage/Build/iOS/ShareClient.framework/ShareClient differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Headers/SwiftCharts-Swift.h b/Carthage/Build/iOS/SwiftCharts.framework/Headers/SwiftCharts-Swift.h
index 2d766a6518..a9dc67c0fd 100644
--- a/Carthage/Build/iOS/SwiftCharts.framework/Headers/SwiftCharts-Swift.h
+++ b/Carthage/Build/iOS/SwiftCharts.framework/Headers/SwiftCharts-Swift.h
@@ -1,4 +1,4 @@
-// Generated by Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
+// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
@@ -191,6 +191,13 @@ SWIFT_CLASS("_TtC11SwiftCharts14ChartAreasView")
- (nonnull instancetype)initWithFrame:(CGRect)frame SWIFT_UNAVAILABLE;
@end
+
+SWIFT_CLASS("_TtC11SwiftCharts22ChartBarStackFrameView")
+@interface ChartBarStackFrameView : UIView
+- (nonnull instancetype)initWithFrame:(CGRect)frame OBJC_DESIGNATED_INITIALIZER;
+- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
+@end
+
@class UIPinchGestureRecognizer;
@class UIPanGestureRecognizer;
@class UITapGestureRecognizer;
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Info.plist b/Carthage/Build/iOS/SwiftCharts.framework/Info.plist
index 8d580dc863..cbb33118bc 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Info.plist and b/Carthage/Build/iOS/SwiftCharts.framework/Info.plist differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc
index 12bfcb3030..db6d01c2f4 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule
index 1f460f4f03..2a5de04edd 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc
index e1db2988f3..1d3f6e6454 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule
index 58a8f19db4..487cd359ec 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/arm64.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc
index c197534efd..8af7337ac0 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule
index 97bf59f565..5bcfbd63b4 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/i386.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc
index da01d0c0aa..f99849eaed 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftdoc differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule
index 605b0b3dfa..0cff35b80b 100644
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/SwiftCharts.framework/Modules/SwiftCharts.swiftmodule/x86_64.swiftmodule differ
diff --git a/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts b/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts
index 06c3dd64b0..53fcb1e770 100755
Binary files a/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts and b/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts differ
diff --git a/Documentation/Screenshots Features/Foodpicker - Camera.png b/Documentation/Screenshots Features/Foodpicker - Camera.png
new file mode 100644
index 0000000000..50c797870a
Binary files /dev/null and b/Documentation/Screenshots Features/Foodpicker - Camera.png differ
diff --git a/Documentation/Screenshots Features/Foodpicker - Drinks.png b/Documentation/Screenshots Features/Foodpicker - Drinks.png
new file mode 100644
index 0000000000..97b9b78150
Binary files /dev/null and b/Documentation/Screenshots Features/Foodpicker - Drinks.png differ
diff --git a/Documentation/Screenshots Features/Foodpicker - Multiple.png b/Documentation/Screenshots Features/Foodpicker - Multiple.png
new file mode 100644
index 0000000000..99241926b6
Binary files /dev/null and b/Documentation/Screenshots Features/Foodpicker - Multiple.png differ
diff --git a/Documentation/Screenshots Features/Foodpicker - Single.png b/Documentation/Screenshots Features/Foodpicker - Single.png
new file mode 100644
index 0000000000..1ea94ead3c
Binary files /dev/null and b/Documentation/Screenshots Features/Foodpicker - Single.png differ
diff --git a/Documentation/Screenshots Features/QuickCarbs - Default.png b/Documentation/Screenshots Features/QuickCarbs - Default.png
new file mode 100644
index 0000000000..824c4be651
Binary files /dev/null and b/Documentation/Screenshots Features/QuickCarbs - Default.png differ
diff --git a/Documentation/Screenshots Features/Settings - AutoBolus.png b/Documentation/Screenshots Features/Settings - AutoBolus.png
new file mode 100644
index 0000000000..4d00e51750
Binary files /dev/null and b/Documentation/Screenshots Features/Settings - AutoBolus.png differ
diff --git a/Documentation/Screenshots Features/Settings - Disconnect Sports.png b/Documentation/Screenshots Features/Settings - Disconnect Sports.png
new file mode 100644
index 0000000000..b9211b4404
Binary files /dev/null and b/Documentation/Screenshots Features/Settings - Disconnect Sports.png differ
diff --git a/Documentation/Screenshots Features/Settings - Expertmode.png b/Documentation/Screenshots Features/Settings - Expertmode.png
new file mode 100644
index 0000000000..d331705263
Binary files /dev/null and b/Documentation/Screenshots Features/Settings - Expertmode.png differ
diff --git a/Documentation/Screenshots Features/Settings - MaxIOB MinBasal.png b/Documentation/Screenshots Features/Settings - MaxIOB MinBasal.png
new file mode 100644
index 0000000000..51dbe09864
Binary files /dev/null and b/Documentation/Screenshots Features/Settings - MaxIOB MinBasal.png differ
diff --git a/Documentation/Screenshots Features/Settings - MinBasal.png b/Documentation/Screenshots Features/Settings - MinBasal.png
new file mode 100644
index 0000000000..c4c09b1bbb
Binary files /dev/null and b/Documentation/Screenshots Features/Settings - MinBasal.png differ
diff --git a/Documentation/Screenshots Features/Status - Disconnected.png b/Documentation/Screenshots Features/Status - Disconnected.png
new file mode 100644
index 0000000000..a607aff09f
Binary files /dev/null and b/Documentation/Screenshots Features/Status - Disconnected.png differ
diff --git a/Documentation/Screenshots Features/Status - MealInformation - Automatic Bolus.png b/Documentation/Screenshots Features/Status - MealInformation - Automatic Bolus.png
new file mode 100644
index 0000000000..a9e860f4ac
Binary files /dev/null and b/Documentation/Screenshots Features/Status - MealInformation - Automatic Bolus.png differ
diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift
index f54990834b..73e0d7e313 100644
--- a/DoseMathTests/DoseMathTests.swift
+++ b/DoseMathTests/DoseMathTests.swift
@@ -101,6 +101,14 @@ class RecommendTempBasalTests: XCTestCase {
return TimeInterval(hours: 4)
}
+ var insulinOnBoard: Double {
+ return 0
+ }
+
+ var maxInsulinOnBoard: Double {
+ return 25
+ }
+
func testNoChange() {
let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
@@ -112,6 +120,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -129,6 +139,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -151,6 +163,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: lastTempBasal
)
@@ -169,6 +183,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -190,6 +206,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: lastTempBasal
)
@@ -217,6 +235,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: lastTempBasal
)
@@ -231,6 +251,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -248,6 +270,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -266,6 +290,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -287,6 +313,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: lastTempBasal
)
@@ -305,13 +333,75 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
-
+ // Basal 0.8, 1.1 units required -> 2.2 units extra for 30 minutes
XCTAssertEqual(3.0, dose!.unitsPerHour)
XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
}
+ func testFlatAndHighLimitIob() {
+ let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high")
+
+ let dose = glucose.recommendedTempBasal(
+ to: glucoseTargetRange,
+ at: glucose.first!.startDate,
+ suspendThreshold: suspendThreshold.quantity,
+ sensitivity: insulinSensitivitySchedule,
+ model: insulinModel,
+ basalRates: basalRateSchedule,
+ maxBasalRate: maxBasalRate,
+ insulinOnBoard: 0,
+ maxInsulinOnBoard: 1,
+ lastTempBasal: nil
+ )
+ // Basal 0.8, 1.1 units required, limited to 1 -> 2 units extra for 30 minutes = 2.8
+ XCTAssertEqual(2.8, dose!.unitsPerHour)
+ XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
+ }
+
+ func testFlatAndHighLimitIobWithOnboard() {
+ let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high")
+
+ let dose = glucose.recommendedTempBasal(
+ to: glucoseTargetRange,
+ at: glucose.first!.startDate,
+ suspendThreshold: suspendThreshold.quantity,
+ sensitivity: insulinSensitivitySchedule,
+ model: insulinModel,
+ basalRates: basalRateSchedule,
+ maxBasalRate: maxBasalRate,
+ insulinOnBoard: 1.5,
+ maxInsulinOnBoard: 2,
+ lastTempBasal: nil
+ )
+ // Basal 0.8, 1.1 units required, limited to 0.5 -> 1 units extra for 30 minutes = 1.8
+ XCTAssertEqual(1.8, dose!.unitsPerHour)
+ XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration)
+ }
+
+ func testFlatAndHighLimitIobExceeded() {
+ let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high")
+
+ let dose = glucose.recommendedTempBasal(
+ to: glucoseTargetRange,
+ at: glucose.first!.startDate,
+ suspendThreshold: suspendThreshold.quantity,
+ sensitivity: insulinSensitivitySchedule,
+ model: insulinModel,
+ basalRates: basalRateSchedule,
+ maxBasalRate: maxBasalRate,
+ insulinOnBoard: 2.5,
+ maxInsulinOnBoard: 2,
+ lastTempBasal: nil
+ )
+ // If the IOB is exceeded the rate is limited to the default
+ // basal rate.
+ XCTAssertNil(dose)
+ }
+
func testHighAndFalling() {
let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling")
@@ -323,6 +413,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -341,6 +433,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -359,6 +453,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -376,6 +472,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -394,6 +492,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -412,6 +512,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -429,6 +531,8 @@ class RecommendTempBasalTests: XCTestCase {
model: insulinModel,
basalRates: basalRateSchedule,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
lastTempBasal: nil
)
@@ -487,6 +591,14 @@ class RecommendBolusTests: XCTestCase {
return TimeInterval(hours: 4)
}
+ var insulinOnBoard: Double {
+ return 0
+ }
+
+ var maxInsulinOnBoard: Double {
+ return 25
+ }
+
func testNoChange() {
let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose")
@@ -497,7 +609,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
@@ -513,7 +627,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
@@ -529,7 +645,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
@@ -545,7 +663,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
@@ -561,7 +681,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(1.575, dose.amount)
@@ -573,6 +695,78 @@ class RecommendBolusTests: XCTestCase {
}
}
+ func testStartLowEndHighLimitIob() {
+ let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
+
+ let dose = glucose.recommendedBolus(
+ to: glucoseTargetRange,
+ at: glucose.first!.startDate,
+ suspendThreshold: suspendThreshold.quantity,
+ sensitivity: insulinSensitivitySchedule,
+ model: insulinModel,
+ pendingInsulin: 0,
+ maxBolus: maxBolus,
+ insulinOnBoard: 0,
+ maxInsulinOnBoard: 1.3
+ )
+
+ XCTAssertEqual(1.3, dose.amount)
+
+ if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! {
+ XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60)
+ } else {
+ XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
+ }
+ }
+
+ func testStartLowEndHighLimitIobWithOnboard() {
+ let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
+
+ let dose = glucose.recommendedBolus(
+ to: glucoseTargetRange,
+ at: glucose.first!.startDate,
+ suspendThreshold: suspendThreshold.quantity,
+ sensitivity: insulinSensitivitySchedule,
+ model: insulinModel,
+ pendingInsulin: 0,
+ maxBolus: maxBolus,
+ insulinOnBoard: 1.0,
+ maxInsulinOnBoard: 1.3
+ )
+
+ XCTAssertEqual(0.3, dose.amount)
+
+ if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! {
+ XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60)
+ } else {
+ XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
+ }
+ }
+
+ func testStartLowEndHighLimitIobExceeded() {
+ let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
+
+ let dose = glucose.recommendedBolus(
+ to: glucoseTargetRange,
+ at: glucose.first!.startDate,
+ suspendThreshold: suspendThreshold.quantity,
+ sensitivity: insulinSensitivitySchedule,
+ model: insulinModel,
+ pendingInsulin: 0,
+ maxBolus: maxBolus,
+ insulinOnBoard: 2.0,
+ maxInsulinOnBoard: 1.3
+ )
+
+ XCTAssertEqual(0, dose.amount)
+
+ if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! {
+ XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60)
+ } else {
+ XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)")
+ }
+ }
+
func testStartBelowSuspendThresholdEndHigh() {
// 60 - 200 mg/dL
let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high")
@@ -584,7 +778,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
@@ -607,7 +803,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
@@ -629,7 +827,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(1.4, dose.amount)
@@ -647,7 +847,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 1,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0.575, dose.amount)
@@ -663,7 +865,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
@@ -679,7 +883,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(1.575, dose.amount, accuracy: 1.0 / 40.0)
@@ -695,7 +901,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0.325, dose.amount, accuracy: 1.0 / 40.0)
@@ -711,7 +919,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0.325, dose.amount, accuracy: 1.0 / 40.0)
@@ -725,7 +935,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0.8,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount, accuracy: .ulpOfOne)
@@ -741,7 +953,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0),
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0.275, dose.amount)
@@ -757,7 +971,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: self.insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(1.25, dose.amount)
@@ -772,7 +988,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(1.25, dose.amount, accuracy: 1.0 / 40.0)
@@ -788,7 +1006,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0.0, dose.amount)
@@ -804,7 +1024,9 @@ class RecommendBolusTests: XCTestCase {
sensitivity: insulinSensitivitySchedule,
model: insulinModel,
pendingInsulin: 0,
- maxBolus: maxBolus
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard
)
XCTAssertEqual(0, dose.amount)
diff --git a/DoseMathTests/Info.plist b/DoseMathTests/Info.plist
index 6d86f6a299..ba8d5550a2 100644
--- a/DoseMathTests/Info.plist
+++ b/DoseMathTests/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.5.6
+ 1.9.4.20200329
CFBundleSignature
????
CFBundleVersion
diff --git a/FEATURES.md b/FEATURES.md
new file mode 100644
index 0000000000..b903634355
--- /dev/null
+++ b/FEATURES.md
@@ -0,0 +1,151 @@
+# New Features in this Fork
+
+see [TODO](/TODO.md) for planned features
+
+You likely also want a patched Nightscout version to ignore the extra log entries
+https://github.com/erikdi/cgm-remote-monitor/tree/erikdi/dev .
+
+## Screenshots worth a thousand words
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Automated Bolus Core Infrastructure
+
+- Give automated bolus of 70% of the recommended value.
+ Kind of like SMBs, but faster.
+- Minium of 0.2 units
+- Do not high temp Basal when doing this
+
+## Bolus Guard Features
+
+- Not exceed recommended amount
+- Prefill correct amount in the field
+- take Maximum Insulin on board into account
+- Round to 0.1
+- No touch id
+- Disable bolus and bolus button if a bolus is in progress
+
+## Display ongoing Bolus State (for automated and manual)
+
+- Display of Bolus and interaction with StatusViewController
+- Also display carb recommendations
+
+## Kids/Caregive vs. Expert Mode
+
+- Disable settings by default
+- Need long touch of 2 seconds to enable
+- Disable any Bolus recommendation modification as well as
+ Carb or Insulin Edits
+
+## Automated Bolus
+
+- Maximum IOB
+- Minimum Basal Rate
+- Safe distance of Bolus'
+
+## New Carb Entry View (QuickCarbs)
+
+- Focus on the basics and +- 5 carbs increments
+- In the future allow manual glucose entry.
+- Manual Glucose Entry (plus LoopManager handling if necessary)
+
+
+## Meal Information on main screen
+
+- Shows individually entered carbs to quickly check what
+ was entered, also allow undo of last entry, if no
+ Bolus was given.
+
+## FoodManager
+
+- Add a food database with pictures and slider to select
+ amount of food for easy entry of common food, even for
+ the illiterate. Also keeps better track of what was
+ eaten.
+- Supports liquid, single and multiple selection
+- Absorption time depending on food.
+- Pre-programmed carb ratios
+
+## Minimum Basal Rate
+
+- Allow configuring the minimum basal to prevent going
+ down completely to zero for long amounts of time.
+
+## Note taking feature
+
+- Add a button to the status bar allowing adding random
+ notes to be taken and logged to Nightscout
+
+## Workout - Disconnect Pump Target
+
+- Sets a minimal basal rate to mimic effect of no insulin
+ while the pump is not attached. Could probably be improved
+ by adding a Zero target for this time. Also prevents
+ unintentional automatic bolus.
+
+## Logging of Site Change and Reservoir Change to Nightscout
+
+- Logs customs notes
+- Treats "Canula fill" as a Site Change
+- Treats "Reservoir fill" as an Insulin Change
+- Consider using the forked Nightscout version to get filtering
+ of the log entries:
+ https://github.com/erikdi/cgm-remote-monitor/tree/erikdi/dev
+
+## Logging of current Profile to Nightscout
+
+- Automatically log the current Basal, CarbRatio and Target
+ settings to Nightscout.
+- Store in Nightscout: Minimum Basal Rates, Workout/Meal Targets, G5 Transmitter ID, Pump ID
+
+## Predictive Low Notifications
+
+- Generate a Notification if the predicted glucose value is
+ going to be below the guard value in the next 30 minutes.
+
+## Retries of Pump operations
+
+- TempBasal, Bolus, readPumpStatus will all be (safely)
+ retried to prevent the amount of times the user
+ will need to manually retry and improve the chance of
+ a successful loop with more challenging RF
+ environments.
+
+## Automated Time change on pump
+
+- If the difference is more than a few seconds, synchronize time.
+
+## Recommend Bolus based on Carbs if no glucose is available
+
+- Implemented and working. Use at your own risk. Won't trigger
+ an automated Bolus.
+
+## Bluetooth restart
+
+- Triggered if either Glucose or Rileylink is missing. Not super reliable.
+
+## Logging of G5/6 Sensor information
+
+- Logs sensor start date and calibration information to Nightscout.
+
+# Additional Features for consideration
+
+## Don't turn off bolus below min guard
+
+Just reduce it a lot.
+
+## Display more info for bolus
+
+Like the progress, especially for bigger bolus amounts would be useful.
+
diff --git a/Loop Status Extension/Info.plist b/Loop Status Extension/Info.plist
index 9f091d68bf..3f41480cfc 100644
--- a/Loop Status Extension/Info.plist
+++ b/Loop Status Extension/Info.plist
@@ -2,12 +2,12 @@
+ AppGroupIdentifier
+ $(APP_GROUP_IDENTIFIER)
CFBundleDevelopmentRegion
en
- MainAppBundleIdentifier
- $(MAIN_APP_BUNDLE_IDENTIFIER)
CFBundleDisplayName
- Loop
+ Loop2
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -19,11 +19,11 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- 1.5.6
+ 1.9.4.20200329
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
- AppGroupIdentifier
- $(APP_GROUP_IDENTIFIER)
+ MainAppBundleIdentifier
+ $(MAIN_APP_BUNDLE_IDENTIFIER)
NSExtension
NSExtensionMainStoryboard
diff --git a/Loop Status Extension/Loop Status Extension.entitlements b/Loop Status Extension/Loop Status Extension.entitlements
index d9849a816d..9c1339dbda 100644
--- a/Loop Status Extension/Loop Status Extension.entitlements
+++ b/Loop Status Extension/Loop Status Extension.entitlements
@@ -4,7 +4,7 @@
com.apple.security.application-groups
- $(APP_GROUP_IDENTIFIER)
+ group.net.loopkit.loop2.LoopGroup
diff --git a/Loop.xcconfig b/Loop.xcconfig
index d092e89a38..d0529b213e 100644
--- a/Loop.xcconfig
+++ b/Loop.xcconfig
@@ -9,3 +9,4 @@
// Change this on first setup to your own unique organization identifier in
// reverse-domain name syntax.
MAIN_APP_BUNDLE_IDENTIFIER = com.loopkit
+DEVELOPMENT_TEAM = ""
diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj
index 573e187130..a34fc264e7 100644
--- a/Loop.xcodeproj/project.pbxproj
+++ b/Loop.xcodeproj/project.pbxproj
@@ -174,6 +174,31 @@
43F78D4D1C914197002152D1 /* GlucoseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D491C914197002152D1 /* GlucoseKit.framework */; };
43F78D4F1C914197002152D1 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F78D4B1C914197002152D1 /* LoopKit.framework */; };
43FCBBC21E51710B00343C1B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43776F9A1B8022E90074EA36 /* LaunchScreen.storyboard */; };
+ 492FFB2E20AA3298000A90E3 /* GitInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 492FFB2D20AA3298000A90E3 /* GitInfo.plist */; };
+ 492FFB3120AA345B000A90E3 /* GitVersionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492FFB3020AA345B000A90E3 /* GitVersionInformation.swift */; };
+ 493BDE11201366B8004967E4 /* FoodPickerCameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE0F201366B7004967E4 /* FoodPickerCameraViewController.swift */; };
+ 493BDE12201366B8004967E4 /* FoodPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE10201366B7004967E4 /* FoodPickerViewController.swift */; };
+ 493BDE14201366CC004967E4 /* FoodManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE13201366CC004967E4 /* FoodManager.swift */; };
+ 493BDE16201366FC004967E4 /* NoteTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE15201366FC004967E4 /* NoteTableViewController.swift */; };
+ 493BDE1820136706004967E4 /* QuickCarbEntryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE1720136706004967E4 /* QuickCarbEntryViewController.swift */; };
+ 493BDE1C20136724004967E4 /* FoodCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE1920136723004967E4 /* FoodCollectionReusableView.swift */; };
+ 493BDE1D20136724004967E4 /* FoodRecentCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE1A20136723004967E4 /* FoodRecentCollectionView.swift */; };
+ 493BDE1E20136724004967E4 /* FoodCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE1B20136724004967E4 /* FoodCollectionViewCell.swift */; };
+ 493BDE2020136733004967E4 /* MealTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE1F20136733004967E4 /* MealTableViewCell.swift */; };
+ 493BDE222013676E004967E4 /* FoodCatalog in Resources */ = {isa = PBXBuildFile; fileRef = 493BDE212013676D004967E4 /* FoodCatalog */; };
+ 493BDE2420137179004967E4 /* CarbEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE2320137178004967E4 /* CarbEntry.swift */; };
+ 493BDE282013A6D5004967E4 /* TreatmentInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE272013A6D5004967E4 /* TreatmentInformation.swift */; };
+ 493BDE2F201A8B58004967E4 /* BluetoothManagerHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 493BDE2D201A8B57004967E4 /* BluetoothManagerHandler.m */; };
+ 4988FFDC201A98C9007967AC /* NightscoutUploader+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4988FFDB201A98C9007967AC /* NightscoutUploader+Profile.swift */; };
+ 4988FFDF20274A6E007967AC /* future_low.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFDE20274A6E007967AC /* future_low.wav */; };
+ 4988FFE320274EAA007967AC /* ding1.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFE020274EA8007967AC /* ding1.wav */; };
+ 4988FFE420274EAA007967AC /* ding3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFE120274EA9007967AC /* ding3.wav */; };
+ 4988FFE520274EAA007967AC /* ding2.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFE220274EA9007967AC /* ding2.wav */; };
+ 4988FFEA20274F23007967AC /* beep3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFE620274F21007967AC /* beep3.wav */; };
+ 4988FFEB20274F23007967AC /* beep2.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFE720274F21007967AC /* beep2.wav */; };
+ 4988FFEC20274F23007967AC /* beep1.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFE820274F22007967AC /* beep1.wav */; };
+ 4988FFED20274F23007967AC /* error.wav in Resources */ = {isa = PBXBuildFile; fileRef = 4988FFE920274F23007967AC /* error.wav */; };
+ 49F4C5B220A22CC500E42156 /* StatisticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F4C5B120A22CC500E42156 /* StatisticsManager.swift */; };
4D3B40041D4A9E1A00BC6334 /* G4ShareSpy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */; };
4D5B7A4B1D457CCA00796CA9 /* GlucoseG4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */; };
4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F08DE8E1E7BB871006741EA /* CollectionType+Loop.swift */; };
@@ -447,7 +472,7 @@
436A0DA41D236A2A00104B24 /* LoopError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopError.swift; sourceTree = ""; };
436D9BF71F6F4EA100CFA75F /* recommended_temp_start_low_end_just_above_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommended_temp_start_low_end_just_above_range.json; sourceTree = ""; };
436FACED1D0BA636004E2427 /* InsulinDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDataSource.swift; sourceTree = ""; };
- 43776F8C1B8022E90074EA36 /* Loop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Loop.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 43776F8C1B8022E90074EA36 /* Loop2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Loop2.app; sourceTree = BUILT_PRODUCTS_DIR; };
43776F8F1B8022E90074EA36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
43776F961B8022E90074EA36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
43776F981B8022E90074EA36 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
@@ -565,6 +590,36 @@
43F78D491C914197002152D1 /* GlucoseKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GlucoseKit.framework; path = Carthage/Build/iOS/GlucoseKit.framework; sourceTree = ""; };
43F78D4B1C914197002152D1 /* LoopKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LoopKit.framework; path = Carthage/Build/iOS/LoopKit.framework; sourceTree = SOURCE_ROOT; };
43FBEDD71D73843700B21F22 /* LevelMaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LevelMaskView.swift; sourceTree = ""; };
+ 492FFB2D20AA3298000A90E3 /* GitInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = GitInfo.plist; sourceTree = ""; };
+ 492FFB3020AA345B000A90E3 /* GitVersionInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitVersionInformation.swift; sourceTree = ""; };
+ 493BDE0C20114DBF004967E4 /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = ""; };
+ 493BDE0F201366B7004967E4 /* FoodPickerCameraViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodPickerCameraViewController.swift; sourceTree = ""; };
+ 493BDE10201366B7004967E4 /* FoodPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodPickerViewController.swift; sourceTree = ""; };
+ 493BDE13201366CC004967E4 /* FoodManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodManager.swift; sourceTree = ""; };
+ 493BDE15201366FC004967E4 /* NoteTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteTableViewController.swift; sourceTree = ""; };
+ 493BDE1720136706004967E4 /* QuickCarbEntryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickCarbEntryViewController.swift; sourceTree = ""; };
+ 493BDE1920136723004967E4 /* FoodCollectionReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodCollectionReusableView.swift; sourceTree = ""; };
+ 493BDE1A20136723004967E4 /* FoodRecentCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodRecentCollectionView.swift; sourceTree = ""; };
+ 493BDE1B20136724004967E4 /* FoodCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodCollectionViewCell.swift; sourceTree = ""; };
+ 493BDE1F20136733004967E4 /* MealTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealTableViewCell.swift; sourceTree = ""; };
+ 493BDE212013676D004967E4 /* FoodCatalog */ = {isa = PBXFileReference; lastKnownFileType = folder; path = FoodCatalog; sourceTree = ""; };
+ 493BDE2320137178004967E4 /* CarbEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntry.swift; sourceTree = ""; };
+ 493BDE272013A6D5004967E4 /* TreatmentInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentInformation.swift; sourceTree = ""; };
+ 493BDE2A201A8B52004967E4 /* Loop2-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Loop2-Bridging-Header.h"; sourceTree = ""; };
+ 493BDE2B201A8B56004967E4 /* Bluetooth-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bluetooth-Bridging-Header.h"; sourceTree = ""; };
+ 493BDE2C201A8B57004967E4 /* BluetoothManagerHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BluetoothManagerHandler.h; sourceTree = ""; };
+ 493BDE2D201A8B57004967E4 /* BluetoothManagerHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BluetoothManagerHandler.m; sourceTree = ""; };
+ 493BDE2E201A8B58004967E4 /* BluetoothManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BluetoothManager.h; sourceTree = ""; };
+ 4988FFDB201A98C9007967AC /* NightscoutUploader+Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NightscoutUploader+Profile.swift"; sourceTree = ""; };
+ 4988FFDE20274A6E007967AC /* future_low.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = future_low.wav; sourceTree = ""; };
+ 4988FFE020274EA8007967AC /* ding1.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ding1.wav; sourceTree = ""; };
+ 4988FFE120274EA9007967AC /* ding3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ding3.wav; sourceTree = ""; };
+ 4988FFE220274EA9007967AC /* ding2.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ding2.wav; sourceTree = ""; };
+ 4988FFE620274F21007967AC /* beep3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = beep3.wav; sourceTree = ""; };
+ 4988FFE720274F21007967AC /* beep2.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = beep2.wav; sourceTree = ""; };
+ 4988FFE820274F22007967AC /* beep1.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = beep1.wav; sourceTree = ""; };
+ 4988FFE920274F23007967AC /* error.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = error.wav; sourceTree = ""; };
+ 49F4C5B120A22CC500E42156 /* StatisticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsManager.swift; sourceTree = ""; };
4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = G4ShareSpy.framework; path = Carthage/Build/iOS/G4ShareSpy.framework; sourceTree = SOURCE_ROOT; };
4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GlucoseG4.swift; path = Loop/Models/GlucoseG4.swift; sourceTree = SOURCE_ROOT; };
4F08DE7C1E7BB6E5006741EA /* ChartAxisValueDoubleLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartAxisValueDoubleLog.swift; sourceTree = ""; };
@@ -770,6 +825,7 @@
C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */,
4309786D1E73DAD100BEBC82 /* CGM.swift */,
540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */,
+ 492FFB3020AA345B000A90E3 /* GitVersionInformation.swift */,
43E397A21D56B9E40028E321 /* Glucose.swift */,
43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */,
4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */,
@@ -783,6 +839,7 @@
43D848AF1E7DCBE100DADCBC /* Result.swift */,
43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */,
43441A9B1EDB34810087958C /* StatusExtensionContext+LoopKit.swift */,
+ 493BDE272013A6D5004967E4 /* TreatmentInformation.swift */,
4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */,
);
path = Models;
@@ -791,6 +848,7 @@
43776F831B8022E90074EA36 = {
isa = PBXGroup;
children = (
+ 493BDE0C20114DBF004967E4 /* TODO.md */,
4FF4D0FA1E1834BD00846527 /* Common */,
43776F8E1B8022E90074EA36 /* Loop */,
4F70C1DF1DE8DCA7006380B7 /* Loop Status Extension */,
@@ -808,7 +866,7 @@
43776F8D1B8022E90074EA36 /* Products */ = {
isa = PBXGroup;
children = (
- 43776F8C1B8022E90074EA36 /* Loop.app */,
+ 43776F8C1B8022E90074EA36 /* Loop2.app */,
43A943721B926B7B0051FA24 /* WatchApp.app */,
43A9437E1B926B7B0051FA24 /* WatchApp Extension.appex */,
43E2D8D11D20BF42004DA55F /* DoseMathTests.xctest */,
@@ -822,12 +880,15 @@
43776F8E1B8022E90074EA36 /* Loop */ = {
isa = PBXGroup;
children = (
+ 4988FFDD20274A57007967AC /* Sounds */,
+ 493BDE212013676D004967E4 /* FoodCatalog */,
7D7076651FE06EE4004AC8EA /* Localizable.strings */,
7D7076511FE06EE1004AC8EA /* InfoPlist.strings */,
C9886AE41E5B2FAD00473BB8 /* gallery.ckcomplication */,
4309786B1E73D2F500BEBC82 /* it.lproj */,
43EDEE6B1CF2E12A00393BE3 /* Loop.entitlements */,
43F5C2D41B92A4A6003EB13D /* Info.plist */,
+ 492FFB2D20AA3298000A90E3 /* GitInfo.plist */,
43776F8F1B8022E90074EA36 /* AppDelegate.swift */,
43776F981B8022E90074EA36 /* Assets.xcassets */,
43776F9A1B8022E90074EA36 /* LaunchScreen.storyboard */,
@@ -943,6 +1004,7 @@
isa = PBXGroup;
children = (
C17824991E1999FA00D9D25C /* CaseCountable.swift */,
+ 493BDE2320137178004967E4 /* CarbEntry.swift */,
4F6663931E905FD2009E74FC /* ChartColorPalette+Loop.swift */,
4346D1F51C78501000ABAFE3 /* ChartPoint+Loop.swift */,
4389916A1E91B689000EEF90 /* ChartSettings+Loop.swift */,
@@ -953,6 +1015,7 @@
C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */,
438172D81F4E9E37003C3328 /* NewPumpEvent.swift */,
43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */,
+ 4988FFDB201A98C9007967AC /* NightscoutUploader+Profile.swift */,
43E344A31B9E1B1C00C85C07 /* NSUserDefaults.swift */,
43441A9F1EDB4D390087958C /* OSLog.swift */,
4381D2251F3C0FDD004ACCF9 /* RileyLinkDevice.swift */,
@@ -976,11 +1039,15 @@
43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */,
43A51E201EB6DBDD000736CC /* ChartsTableViewController.swift */,
433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */,
+ 493BDE0F201366B7004967E4 /* FoodPickerCameraViewController.swift */,
+ 493BDE10201366B7004967E4 /* FoodPickerViewController.swift */,
C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */,
4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */,
435CB6221F37967800C320C7 /* InsulinModelSettingsViewController.swift */,
+ 493BDE15201366FC004967E4 /* NoteTableViewController.swift */,
437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */,
433EA4C11D9F39C900CD78FB /* PumpIDTableViewController.swift */,
+ 493BDE1720136706004967E4 /* QuickCarbEntryViewController.swift */,
43F5173C1D713DB0000FA422 /* RadioSelectionTableViewController.swift */,
43F5C2DA1B92A5E1003EB13D /* SettingsTableViewController.swift */,
43E3449E1B9D68E900C85C07 /* StatusTableViewController.swift */,
@@ -999,8 +1066,12 @@
43B260481ED248FB008CAA77 /* CarbEntryTableViewCell.swift */,
4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */,
431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */,
+ 493BDE1920136723004967E4 /* FoodCollectionReusableView.swift */,
+ 493BDE1B20136724004967E4 /* FoodCollectionViewCell.swift */,
+ 493BDE1A20136723004967E4 /* FoodRecentCollectionView.swift */,
43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */,
430D85881F44037000AF2D4F /* HUDViewTableViewCell.swift */,
+ 493BDE1F20136733004967E4 /* MealTableViewCell.swift */,
438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */,
43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */,
43F64DD81D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift */,
@@ -1013,12 +1084,14 @@
43F5C2E41B93C5D4003EB13D /* Managers */ = {
isa = PBXGroup;
children = (
+ 493BDE29201A8B39004967E4 /* Bluetooth */,
439BED281E76091600B0AED5 /* CGM */,
439897361CD2F80600223065 /* AnalyticsManager.swift */,
43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */,
43F4EF1C1BA2A57600526CE1 /* DiagnosticLogger.swift */,
4315D2891CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift */,
43F78D251C8FC000002152D1 /* DoseMath.swift */,
+ 493BDE13201366CC004967E4 /* FoodManager.swift */,
4F08DEA01E81D90F006741EA /* GlucoseRangeScheduleCalculator.swift */,
43E2D8C51D204678004DA55F /* KeychainManager.swift */,
43E2D8C71D208D5B004DA55F /* KeychainManager+Loop.swift */,
@@ -1027,6 +1100,7 @@
43C094491CACCC73001F6403 /* NotificationManager.swift */,
432E73CA1D24B3D6009AD15D /* RemoteDataManager.swift */,
43045E571F25AC1700FD9CE1 /* RileyLinkDeviceManager.swift */,
+ 49F4C5B120A22CC500E42156 /* StatisticsManager.swift */,
430C1ABC1E5568A80067F1AE /* StatusChartsManager+LoopKit.swift */,
4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */,
4328E0341CFC0AE100E199AA /* WatchDataManager.swift */,
@@ -1043,6 +1117,33 @@
path = LoopTests;
sourceTree = "";
};
+ 493BDE29201A8B39004967E4 /* Bluetooth */ = {
+ isa = PBXGroup;
+ children = (
+ 493BDE2B201A8B56004967E4 /* Bluetooth-Bridging-Header.h */,
+ 493BDE2E201A8B58004967E4 /* BluetoothManager.h */,
+ 493BDE2C201A8B57004967E4 /* BluetoothManagerHandler.h */,
+ 493BDE2D201A8B57004967E4 /* BluetoothManagerHandler.m */,
+ 493BDE2A201A8B52004967E4 /* Loop2-Bridging-Header.h */,
+ );
+ path = Bluetooth;
+ sourceTree = "";
+ };
+ 4988FFDD20274A57007967AC /* Sounds */ = {
+ isa = PBXGroup;
+ children = (
+ 4988FFDE20274A6E007967AC /* future_low.wav */,
+ 4988FFE820274F22007967AC /* beep1.wav */,
+ 4988FFE720274F21007967AC /* beep2.wav */,
+ 4988FFE620274F21007967AC /* beep3.wav */,
+ 4988FFE920274F23007967AC /* error.wav */,
+ 4988FFE020274EA8007967AC /* ding1.wav */,
+ 4988FFE220274EA9007967AC /* ding2.wav */,
+ 4988FFE120274EA9007967AC /* ding3.wav */,
+ );
+ path = Sounds;
+ sourceTree = "";
+ };
4F70C1DF1DE8DCA7006380B7 /* Loop Status Extension */ = {
isa = PBXGroup;
children = (
@@ -1208,10 +1309,11 @@
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
- 43776F8B1B8022E90074EA36 /* Loop */ = {
+ 43776F8B1B8022E90074EA36 /* Loop2 */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 43776FB61B8022E90074EA36 /* Build configuration list for PBXNativeTarget "Loop" */;
+ buildConfigurationList = 43776FB61B8022E90074EA36 /* Build configuration list for PBXNativeTarget "Loop2" */;
buildPhases = (
+ 492FFB2C20AA2BD4000A90E3 /* Git Hash */,
43776F881B8022E90074EA36 /* Sources */,
43776F891B8022E90074EA36 /* Frameworks */,
43776F8A1B8022E90074EA36 /* Resources */,
@@ -1228,9 +1330,9 @@
43A943931B926B7B0051FA24 /* PBXTargetDependency */,
4F70C1E71DE8DCA7006380B7 /* PBXTargetDependency */,
);
- name = Loop;
+ name = Loop2;
productName = Loop;
- productReference = 43776F8C1B8022E90074EA36 /* Loop.app */;
+ productReference = 43776F8C1B8022E90074EA36 /* Loop2.app */;
productType = "com.apple.product-type.application";
};
43A943711B926B7B0051FA24 /* WatchApp */ = {
@@ -1353,7 +1455,7 @@
TargetAttributes = {
43776F8B1B8022E90074EA36 = {
CreatedOnToolsVersion = 7.0;
- LastSwiftMigration = 0800;
+ LastSwiftMigration = 0920;
SystemCapabilities = {
com.apple.ApplicationGroups.iOS = {
enabled = 1;
@@ -1428,15 +1530,19 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
+ English,
en,
Base,
+ es,
+ ru,
+ it,
);
mainGroup = 43776F831B8022E90074EA36;
productRefGroup = 43776F8D1B8022E90074EA36 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
- 43776F8B1B8022E90074EA36 /* Loop */,
+ 43776F8B1B8022E90074EA36 /* Loop2 */,
4F70C1DB1DE8DCA7006380B7 /* Loop Status Extension */,
43A943711B926B7B0051FA24 /* WatchApp */,
43A9437D1B926B7B0051FA24 /* WatchApp Extension */,
@@ -1452,13 +1558,23 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 492FFB2E20AA3298000A90E3 /* GitInfo.plist in Resources */,
+ 493BDE222013676E004967E4 /* FoodCatalog in Resources */,
+ 4988FFE420274EAA007967AC /* ding3.wav in Resources */,
+ 4988FFDF20274A6E007967AC /* future_low.wav in Resources */,
+ 4988FFEB20274F23007967AC /* beep2.wav in Resources */,
+ 4988FFEC20274F23007967AC /* beep1.wav in Resources */,
+ 4988FFED20274F23007967AC /* error.wav in Resources */,
+ 4988FFEA20274F23007967AC /* beep3.wav in Resources */,
43FCBBC21E51710B00343C1B /* LaunchScreen.storyboard in Resources */,
7D70764F1FE06EE1004AC8EA /* InfoPlist.strings in Resources */,
+ 4988FFE320274EAA007967AC /* ding1.wav in Resources */,
43776F991B8022E90074EA36 /* Assets.xcassets in Resources */,
434F54591D28805E002A9274 /* ButtonTableViewCell.xib in Resources */,
7D7076631FE06EE4004AC8EA /* Localizable.strings in Resources */,
43776F971B8022E90074EA36 /* Main.storyboard in Resources */,
C9886AE51E5B2FAD00473BB8 /* gallery.ckcomplication in Resources */,
+ 4988FFE520274EAA007967AC /* ding2.wav in Resources */,
4309786C1E73D2F500BEBC82 /* it.lproj in Resources */,
434F545B1D2880D4002A9274 /* AuthenticationTableViewCell.xib in Resources */,
);
@@ -1601,6 +1717,20 @@
shellPath = /bin/sh;
shellScript = "# Rebuild using cache if we're developing in a workspace\nif [ -d $PROJECT_DIR/Loop2.xcworkspace ]; then\n /usr/local/bin/carthage build --platform \"$PLATFORM_NAME\" --cache-builds --project-directory \"$SRCROOT\"\nfi\n\n/usr/local/bin/carthage copy-frameworks";
};
+ 492FFB2C20AA2BD4000A90E3 /* Git Hash */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Git Hash";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "DESCRIBE=`git describe --tags`\nCOMMIT=`git rev-parse HEAD`\nBRANCH=`git symbolic-ref --short HEAD`\nrm \"${PROJECT_DIR}/Loop/GitInfo.plist\"\n/usr/libexec/PlistBuddy -c \"Add :GIT_COMMIT_HASH string ${COMMIT}\" \"${PROJECT_DIR}/Loop/GitInfo.plist\"\n/usr/libexec/PlistBuddy -c \"Add :GIT_BRANCH string ${BRANCH}\" \"${PROJECT_DIR}/Loop/GitInfo.plist\"\n/usr/libexec/PlistBuddy -c \"Add :GIT_DESCRIBE string ${DESCRIBE}\" \"${PROJECT_DIR}/Loop/GitInfo.plist\"\n/usr/libexec/PlistBuddy -c \"Add :BUILD_CURRENT_DATE string `date`\" \"${PROJECT_DIR}/Loop/GitInfo.plist\"\n\n";
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -1611,7 +1741,10 @@
C17824A51E1AD4D100D9D25C /* BolusRecommendation.swift in Sources */,
4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */,
434F54571D287FDB002A9274 /* NibLoadable.swift in Sources */,
+ 493BDE1D20136724004967E4 /* FoodRecentCollectionView.swift in Sources */,
+ 493BDE282013A6D5004967E4 /* TreatmentInformation.swift in Sources */,
43441A9C1EDB34810087958C /* StatusExtensionContext+LoopKit.swift in Sources */,
+ 49F4C5B220A22CC500E42156 /* StatisticsManager.swift in Sources */,
4FF4D1001E18374700846527 /* WatchContext.swift in Sources */,
4381D2261F3C0FDD004ACCF9 /* RileyLinkDevice.swift in Sources */,
4315D28A1CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift in Sources */,
@@ -1619,6 +1752,7 @@
4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */,
4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */,
430DA58E1D4AEC230097D1CA /* NSBundle.swift in Sources */,
+ 493BDE14201366CC004967E4 /* FoodManager.swift in Sources */,
43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */,
43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */,
43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */,
@@ -1634,6 +1768,7 @@
43A5676B1C96155700334FAC /* SwitchTableViewCell.swift in Sources */,
43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */,
43D848B01E7DCBE100DADCBC /* Result.swift in Sources */,
+ 493BDE2420137179004967E4 /* CarbEntry.swift in Sources */,
43E397A31D56B9E40028E321 /* Glucose.swift in Sources */,
43B260491ED248FB008CAA77 /* CarbEntryTableViewCell.swift in Sources */,
4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */,
@@ -1650,9 +1785,11 @@
435CB6251F37ABFC00C320C7 /* ExponentialInsulinModelPreset.swift in Sources */,
4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */,
437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */,
+ 493BDE2020136733004967E4 /* MealTableViewCell.swift in Sources */,
43DBF0591C93F73800B3C386 /* CarbEntryTableViewController.swift in Sources */,
43E93FB71E469A5100EAB8DB /* HKUnit.swift in Sources */,
43BFF0BC1E45C80600FF19A9 /* UIColor+Loop.swift in Sources */,
+ 493BDE11201366B8004967E4 /* FoodPickerCameraViewController.swift in Sources */,
43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */,
4F08DE9D1E81D0E9006741EA /* StatusChartsManager+LoopKit.swift in Sources */,
434FF1EE1CF27EEF000DB779 /* UITableViewCell.swift in Sources */,
@@ -1661,11 +1798,14 @@
438849EA1D297CB6003B3F23 /* NightscoutService.swift in Sources */,
438172D91F4E9E37003C3328 /* NewPumpEvent.swift in Sources */,
437CCADC1D284B830075D2C3 /* ButtonTableViewCell.swift in Sources */,
+ 493BDE2F201A8B58004967E4 /* BluetoothManagerHandler.m in Sources */,
4389916B1E91B689000EEF90 /* ChartSettings+Loop.swift in Sources */,
4315D2871CA5CC3B00589052 /* CarbEntryEditTableViewController.swift in Sources */,
4309786E1E73DAD100BEBC82 /* CGM.swift in Sources */,
+ 492FFB3120AA345B000A90E3 /* GitVersionInformation.swift in Sources */,
43F5173D1D713DB0000FA422 /* RadioSelectionTableViewController.swift in Sources */,
C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */,
+ 493BDE16201366FC004967E4 /* NoteTableViewController.swift in Sources */,
4F08DEA11E81D90F006741EA /* GlucoseRangeScheduleCalculator.swift in Sources */,
43DBF04C1C93B8D700B3C386 /* BolusViewController.swift in Sources */,
4FB76FBB1E8C42CF00B39636 /* UIColor.swift in Sources */,
@@ -1675,6 +1815,7 @@
43BFF0C51E465A2D00FF19A9 /* UIColor+HIG.swift in Sources */,
4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */,
437CCAE01D285C7B0075D2C3 /* ServiceAuthentication.swift in Sources */,
+ 4988FFDC201A98C9007967AC /* NightscoutUploader+Profile.swift in Sources */,
4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */,
4302F4E51D4EA75100F0FCAF /* DoseStore.swift in Sources */,
430DA5901D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift in Sources */,
@@ -1708,13 +1849,17 @@
431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */,
439897371CD2F80600223065 /* AnalyticsManager.swift in Sources */,
430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */,
+ 493BDE1E20136724004967E4 /* FoodCollectionViewCell.swift in Sources */,
43A51E211EB6DBDD000736CC /* ChartsTableViewController.swift in Sources */,
+ 493BDE12201366B8004967E4 /* FoodPickerViewController.swift in Sources */,
4346D1F61C78501000ABAFE3 /* ChartPoint+Loop.swift in Sources */,
438849EE1D2A1EBB003B3F23 /* MLabService.swift in Sources */,
+ 493BDE1820136706004967E4 /* QuickCarbEntryViewController.swift in Sources */,
43D848B21E7DF42500DADCBC /* LoopSettings.swift in Sources */,
438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */,
43F4EF1D1BA2A57600526CE1 /* DiagnosticLogger.swift in Sources */,
432E73CB1D24B3D6009AD15D /* RemoteDataManager.swift in Sources */,
+ 493BDE1C20136724004967E4 /* FoodCollectionReusableView.swift in Sources */,
43DE92591C5479E4001FFDE1 /* CarbEntryUserInfo.swift in Sources */,
434F54631D28DD80002A9274 /* ValidatingIndicatorView.swift in Sources */,
43DE92611C555C26001FFDE1 /* AbsorptionTimeType+CarbKit.swift in Sources */,
@@ -1850,7 +1995,7 @@
};
43E2D9111D20C581004DA55F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = 43776F8B1B8022E90074EA36 /* Loop */;
+ target = 43776F8B1B8022E90074EA36 /* Loop2 */;
targetProxy = 43E2D9101D20C581004DA55F /* PBXContainerItemProxy */;
};
4F70C1E71DE8DCA7006380B7 /* PBXTargetDependency */ = {
@@ -2028,7 +2173,7 @@
baseConfigurationReference = 437D9BA11D7B5203007245E8 /* Loop.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- APP_GROUP_IDENTIFIER = "group.$(MAIN_APP_BUNDLE_IDENTIFIER)Group";
+ APP_GROUP_IDENTIFIER = group.net.loopkit.loop2.LoopGroup;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -2055,7 +2200,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer: loudnate@gmail.com (XZN842LDLT)";
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 48;
+ CURRENT_PROJECT_VERSION = 61;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -2077,10 +2222,11 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MAIN_APP_BUNDLE_IDENTIFIER = "$(inherited).Loop";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
+ PRODUCT_NAME = Loop2;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
@@ -2095,7 +2241,7 @@
baseConfigurationReference = 437D9BA11D7B5203007245E8 /* Loop.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- APP_GROUP_IDENTIFIER = "group.$(MAIN_APP_BUNDLE_IDENTIFIER)Group";
+ APP_GROUP_IDENTIFIER = group.net.loopkit.loop2.LoopGroup;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -2122,7 +2268,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer: loudnate@gmail.com (XZN842LDLT)";
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 48;
+ CURRENT_PROJECT_VERSION = 61;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -2138,9 +2284,10 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MAIN_APP_BUNDLE_IDENTIFIER = "$(inherited).Loop";
MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_NAME = Loop2;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.0;
@@ -2156,15 +2303,22 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Loop/Loop.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(PROJECT_DIR)/Carthage/Build/iOS",
+ "$(inherited)",
+ );
INFOPLIST_FILE = Loop/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
"OTHER_SWIFT_FLAGS[sdk=iphonesimulator*]" = "-D IOS_SIMULATOR";
PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Loop/Managers/Bluetooth/Loop2-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
@@ -2173,14 +2327,20 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Loop/Loop.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(PROJECT_DIR)/Carthage/Build/iOS",
+ "$(inherited)",
+ );
INFOPLIST_FILE = Loop/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Loop/Managers/Bluetooth/Loop2-Bridging-Header.h";
};
name = Release;
};
@@ -2191,7 +2351,7 @@
CODE_SIGN_ENTITLEMENTS = "WatchApp Extension/WatchApp Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
INFOPLIST_FILE = "WatchApp Extension/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).LoopWatch.watchkitextension";
@@ -2210,7 +2370,7 @@
CODE_SIGN_ENTITLEMENTS = "WatchApp Extension/WatchApp Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
INFOPLIST_FILE = "WatchApp Extension/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).LoopWatch.watchkitextension";
@@ -2229,7 +2389,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
IBSC_MODULE = WatchApp_Extension;
INFOPLIST_FILE = WatchApp/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -2249,7 +2409,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
IBSC_MODULE = WatchApp_Extension;
INFOPLIST_FILE = WatchApp/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -2327,7 +2487,7 @@
CODE_SIGN_ENTITLEMENTS = "Loop Status Extension/Loop Status Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
INFOPLIST_FILE = "Loop Status Extension/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).statuswidget";
@@ -2346,7 +2506,7 @@
CODE_SIGN_ENTITLEMENTS = "Loop Status Extension/Loop Status Extension.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- DEVELOPMENT_TEAM = "";
+ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
INFOPLIST_FILE = "Loop Status Extension/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).statuswidget";
@@ -2364,11 +2524,11 @@
CLANG_WARN_SUSPICIOUS_MOVES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
- CURRENT_PROJECT_VERSION = 48;
+ CURRENT_PROJECT_VERSION = 61;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 48;
+ DYLIB_CURRENT_VERSION = 61;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = LoopUI/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -2391,11 +2551,11 @@
CLANG_WARN_SUSPICIOUS_MOVES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
- CURRENT_PROJECT_VERSION = 48;
+ CURRENT_PROJECT_VERSION = 61;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 48;
+ DYLIB_CURRENT_VERSION = 61;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = LoopUI/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -2420,7 +2580,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- 43776FB61B8022E90074EA36 /* Build configuration list for PBXNativeTarget "Loop" */ = {
+ 43776FB61B8022E90074EA36 /* Build configuration list for PBXNativeTarget "Loop2" */ = {
isa = XCConfigurationList;
buildConfigurations = (
43776FB71B8022E90074EA36 /* Debug */,
diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme
index ab563e3f5a..96cfba406d 100644
--- a/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme
+++ b/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme
@@ -29,8 +29,8 @@
diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/Loop Status Extension.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/Loop Status Extension.xcscheme
index 0cbbc7f5ee..bb9f3d9ed9 100644
--- a/Loop.xcodeproj/xcshareddata/xcschemes/Loop Status Extension.xcscheme
+++ b/Loop.xcodeproj/xcshareddata/xcschemes/Loop Status Extension.xcscheme
@@ -30,8 +30,8 @@
@@ -72,8 +72,8 @@
@@ -92,8 +92,8 @@
diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme
index 301b74132b..8535524a2b 100644
--- a/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme
+++ b/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme
@@ -15,8 +15,8 @@
@@ -44,8 +44,8 @@
@@ -67,8 +67,8 @@
@@ -86,8 +86,8 @@
diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/Notification - WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/Notification - WatchApp.xcscheme
index e440b6f616..255ab39bc7 100644
--- a/Loop.xcodeproj/xcshareddata/xcschemes/Notification - WatchApp.xcscheme
+++ b/Loop.xcodeproj/xcshareddata/xcschemes/Notification - WatchApp.xcscheme
@@ -29,8 +29,8 @@
diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme
index 52c23e5485..06d19df55b 100644
--- a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme
+++ b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme
@@ -29,8 +29,8 @@
diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift
index 6ddc651410..c58a73e7da 100644
--- a/Loop/AppDelegate.swift
+++ b/Loop/AppDelegate.swift
@@ -18,22 +18,43 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
private(set) lazy var deviceManager = DeviceDataManager()
+ private(set) lazy var foodManager = FoodManager()
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+ NSLog("GitVersionInformation \(GitVersionInformation().dict)")
+
window?.tintColor = UIColor.tintColor
NotificationManager.authorize(delegate: self)
+ // Enable local logging of NSLog for later debugging.
+ /*
+ var paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
+ let documentsDirectory = paths[0]
+ let fileName = "\(Date()).log"
+ let logFilePath = (documentsDirectory as NSString).appendingPathComponent(fileName)
+ // Disabled out of file size concerns for now
+ freopen(logFilePath.cString(using: String.Encoding.ascii)!, "a+", stderr)
+ */
+
let bundle = Bundle(for: type(of: self))
DiagnosticLogger.shared = DiagnosticLogger(subsystem: bundle.bundleIdentifier!, version: bundle.shortVersionString)
DiagnosticLogger.shared?.forCategory("AppDelegate").info(#function)
+ DiagnosticLogger.shared?.loopManager = deviceManager.loopManager
AnalyticsManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
+ AnalyticsManager.shared.loopManager = deviceManager.loopManager
+
+ StatisticsManager.shared.loopManager = deviceManager.loopManager
if let navVC = window?.rootViewController as? UINavigationController,
let statusVC = navVC.viewControllers.first as? StatusTableViewController {
statusVC.deviceManager = deviceManager
+ statusVC.foodManager = foodManager
}
+ application.setMinimumBackgroundFetchInterval(300.0)
+
return true
}
@@ -98,3 +119,22 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
completionHandler([.badge, .sound, .alert])
}
}
+
+
+// Watchdog for resetting Bluetooth if needed.
+extension AppDelegate {
+
+ func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
+ NSLog("background-fetch")
+ deviceManager.maybeToggleBluetooth("background-fetch")
+
+ guard let url = URL(string: "http://www.example.com") else { return }
+ URLSession.shared.dataTask(with: url) { (data, response, err) in
+ guard let data = data else { return }
+ NSLog("AppDelegate Download success \(data)")
+ }.resume()
+
+ completionHandler(UIBackgroundFetchResult.newData)
+ }
+
+}
diff --git a/Loop/Assets.xcassets/AppIcon-1.appiconset/Contents.json b/Loop/Assets.xcassets/AppIcon-1.appiconset/Contents.json
new file mode 100644
index 0000000000..d8db8d65fd
--- /dev/null
+++ b/Loop/Assets.xcassets/AppIcon-1.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "size" : "1024x1024",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/App Store Copy@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/App Store Copy@2x.png
deleted file mode 100644
index 189337f741..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/App Store Copy@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json b/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json
index f9383e1e01..dc56ea3c46 100644
--- a/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -3,114 +3,184 @@
{
"size" : "20x20",
"idiom" : "iphone",
- "filename" : "Icon-Notification-40.png",
+ "filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
- "filename" : "Icon-Small-60.png",
+ "filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
- "filename" : "Icon-Small@2x.png",
+ "filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
- "filename" : "Icon-Small@3x.png",
+ "filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
- "filename" : "Icon-Small-40@2x.png",
+ "filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
- "filename" : "Icon-Small-60@2x.png",
+ "filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
- "filename" : "Icon-60@2x.png",
+ "filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
- "filename" : "Icon-60@3x.png",
+ "filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
- "filename" : "Icon-Notification-20.png",
+ "filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
- "filename" : "Icon-Notification-42.png",
+ "filename" : "Icon-App-20x20@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
- "filename" : "Icon-Small.png",
+ "filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
- "filename" : "Icon-Small@2x-1.png",
+ "filename" : "Icon-App-29x29@2x-1.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
- "filename" : "Icon-Notification-41.png",
+ "filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
- "filename" : "Icon-Small-40@2x-1.png",
+ "filename" : "Icon-App-40x40@2x-1.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
- "filename" : "Icon-76.png",
+ "filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
- "filename" : "Icon-76@2x.png",
+ "filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
- "filename" : "Icon-83.5@2x.png",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
- "filename" : "App Store Copy@2x.png",
+ "filename" : "ItunesArtwork@2x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "24x24",
+ "idiom" : "watch",
+ "filename" : "Icon-24@2x.png",
+ "scale" : "2x",
+ "role" : "notificationCenter",
+ "subtype" : "38mm"
+ },
+ {
+ "size" : "27.5x27.5",
+ "idiom" : "watch",
+ "filename" : "Icon-27.5@2x.png",
+ "scale" : "2x",
+ "role" : "notificationCenter",
+ "subtype" : "42mm"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "watch",
+ "filename" : "Icon-29@2x.png",
+ "role" : "companionSettings",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "watch",
+ "filename" : "Icon-29@3x.png",
+ "role" : "companionSettings",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "watch",
+ "filename" : "Icon-40@2x.png",
+ "scale" : "2x",
+ "role" : "appLauncher",
+ "subtype" : "38mm"
+ },
+ {
+ "size" : "44x44",
+ "idiom" : "watch",
+ "filename" : "Icon-44@2x.png",
+ "scale" : "2x",
+ "role" : "longLook",
+ "subtype" : "42mm"
+ },
+ {
+ "size" : "86x86",
+ "idiom" : "watch",
+ "filename" : "Icon-86@2x.png",
+ "scale" : "2x",
+ "role" : "quickLook",
+ "subtype" : "38mm"
+ },
+ {
+ "size" : "98x98",
+ "idiom" : "watch",
+ "filename" : "Icon-98@2x.png",
+ "scale" : "2x",
+ "role" : "quickLook",
+ "subtype" : "42mm"
+ },
+ {
+ "idiom" : "watch-marketing",
+ "size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
+ },
+ "properties" : {
+ "pre-rendered" : true
}
}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png
new file mode 100644
index 0000000000..85668cf4ff
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png
new file mode 100644
index 0000000000..d404287765
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png
new file mode 100644
index 0000000000..f70d7e1d39
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png
new file mode 100644
index 0000000000..9ed2d59e5a
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
new file mode 100644
index 0000000000..d12ede60cb
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-44@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-44@2x.png
new file mode 100644
index 0000000000..e252f02c5d
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-44@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
deleted file mode 100644
index 754271234a..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
deleted file mode 100644
index b08ae598c6..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76.png
deleted file mode 100644
index d916527459..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
deleted file mode 100644
index 266aa5829a..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
deleted file mode 100644
index b2df83a839..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png
new file mode 100644
index 0000000000..db155ae8c6
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png
new file mode 100644
index 0000000000..06cabff49f
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000000..83b1b52e69
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png
new file mode 100644
index 0000000000..aa3cf782e9
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000000..aa3cf782e9
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000000..c27e7a95eb
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000000..0bc4ac2e15
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png
new file mode 100644
index 0000000000..f70d7e1d39
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000000..f70d7e1d39
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000000..9ed2d59e5a
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000000..aa3cf782e9
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png
new file mode 100644
index 0000000000..d12ede60cb
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000000..d12ede60cb
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000000..948f6ed828
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000000..948f6ed828
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000000..6b8fd745d2
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000000..cff57e1e24
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000000..6f36666251
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000000..51f1cd020c
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-20.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-20.png
deleted file mode 100644
index f70accfa03..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-20.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-40.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-40.png
deleted file mode 100644
index b3792921d7..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-40.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-41.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-41.png
deleted file mode 100644
index b3792921d7..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-41.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-42.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-42.png
deleted file mode 100644
index b3792921d7..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-42.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png
deleted file mode 100644
index b9b45b2865..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
deleted file mode 100644
index b9b45b2865..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60.png
deleted file mode 100644
index 0ad6187e0b..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60@2x.png
deleted file mode 100644
index 754271234a..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
deleted file mode 100644
index ec13a4cb23..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png
deleted file mode 100644
index 02e28324c5..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
deleted file mode 100644
index 02e28324c5..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
deleted file mode 100644
index 3659954b6e..0000000000
Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png and /dev/null differ
diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png
new file mode 100644
index 0000000000..9718c77356
Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ
diff --git a/Loop/Assets.xcassets/blood.imageset/Contents.json b/Loop/Assets.xcassets/blood.imageset/Contents.json
new file mode 100644
index 0000000000..4516e63db0
--- /dev/null
+++ b/Loop/Assets.xcassets/blood.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "blood.png"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/blood.imageset/blood.png b/Loop/Assets.xcassets/blood.imageset/blood.png
new file mode 100644
index 0000000000..2cce397565
Binary files /dev/null and b/Loop/Assets.xcassets/blood.imageset/blood.png differ
diff --git a/Loop/Assets.xcassets/fork.imageset/Contents.json b/Loop/Assets.xcassets/fork.imageset/Contents.json
new file mode 100644
index 0000000000..1e4fc587fc
--- /dev/null
+++ b/Loop/Assets.xcassets/fork.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "carbs.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/fork.imageset/carbs.pdf b/Loop/Assets.xcassets/fork.imageset/carbs.pdf
new file mode 100644
index 0000000000..327ec631b5
Binary files /dev/null and b/Loop/Assets.xcassets/fork.imageset/carbs.pdf differ
diff --git a/Loop/Assets.xcassets/pencil.imageset/Contents.json b/Loop/Assets.xcassets/pencil.imageset/Contents.json
new file mode 100644
index 0000000000..12d20d0ac1
--- /dev/null
+++ b/Loop/Assets.xcassets/pencil.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "pencil.png"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Loop/Assets.xcassets/pencil.imageset/pencil.png b/Loop/Assets.xcassets/pencil.imageset/pencil.png
new file mode 100644
index 0000000000..107f592644
Binary files /dev/null and b/Loop/Assets.xcassets/pencil.imageset/pencil.png differ
diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard
index 41ad584e7d..876ba75794 100644
--- a/Loop/Base.lproj/Main.storyboard
+++ b/Loop/Base.lproj/Main.storyboard
@@ -1,11 +1,11 @@
-
-
+
+
-
+
@@ -14,17 +14,17 @@
-
+
-
+
-
-
+
+
-
+
-
+
-
+
diff --git a/Loop/Extensions/CarbEntry.swift b/Loop/Extensions/CarbEntry.swift
new file mode 100644
index 0000000000..ac7eeb8a93
--- /dev/null
+++ b/Loop/Extensions/CarbEntry.swift
@@ -0,0 +1,29 @@
+//
+// CarbEntry.swift
+// Loop
+//
+// Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import CarbKit
+import HealthKit
+
+extension CarbEntry {
+ func foodPicks() -> FoodPicks {
+ var picks = FoodPicks()
+
+ if let foodType = self.foodType {
+ picks = FoodPicks(fromJSON: foodType)
+ }
+ if picks.last == nil {
+ // create generic entry if foodType did not parse
+ let value = quantity.doubleValue(for: HKUnit.gram())
+ // TODO(Erik) This should take selected absorption time into account
+ let foodItem = FoodItem(carbRatio: 1.0, portionSize: value, absorption: .normal, title: "CarbEntry")
+ let foodPick = FoodPick(item: foodItem, ratio: 1, date: startDate)
+ picks.append(foodPick)
+ }
+ return picks
+ }
+}
diff --git a/Loop/Extensions/DoseStore.swift b/Loop/Extensions/DoseStore.swift
index d5b0fd09bc..ccc3126c39 100644
--- a/Loop/Extensions/DoseStore.swift
+++ b/Loop/Extensions/DoseStore.swift
@@ -8,7 +8,53 @@
import InsulinKit
import MinimedKit
+import NightscoutUploadKit
+public enum FakeEventTypes: UInt8 {
+ case note = 0xfe // Must not exist in MinimedKit.PumpEventType!
+ case siteChange = 0xfd
+ case insulinChange = 0xfc
+ case bgReceived = 0xfb
+ case debug = 0xfa
+ case batteryChange = 0xf9
+ case sensorStart = 0xf8
+ case sensorStop = 0xf7
+ case sensorChange = 0xf6
+}
+
+final class PendingTreatmentsQueueManager: IdentifiableClass {
+
+ static var shared = PendingTreatmentsQueueManager()
+
+ public let queue: DispatchQueue = DispatchQueue(label: "com.loopkit.loop.UserDefaults.pendingTreatmentsQueue", qos: .utility)
+ public var pending : [NightscoutTreatment] = []
+
+ public var generation = String(UUID().uuidString.prefix(4))
+
+ private let lock = DispatchSemaphore(value: 1)
+ private var value = 0
+ private var failed = 0
+
+ public func incrementAndGet() -> Int {
+
+ lock.wait()
+ defer { lock.signal() }
+ value += 1
+ return value
+ }
+
+ public func recordFailure() {
+ lock.wait()
+ defer { lock.signal() }
+ failed += 1
+ }
+
+ public func failures() -> Int {
+ lock.wait()
+ defer { lock.signal() }
+ return failed
+ }
+}
// Bridges support for MinimedKit data types
extension LoopDataManager {
@@ -102,4 +148,134 @@ extension LoopDataManager {
addPumpEvents(events, completion: completion)
}
+
+ // Modifications to handle more logging events from App in Nightscout
+ //
+ private func addFakeEvent(_ eventType: FakeEventTypes, _ note: String, _ eventDate: Date? = nil) {
+ let date = eventDate ?? Date()
+
+ let author = "loop://\(UIDevice.current.name)"
+ let id = PendingTreatmentsQueueManager.shared.generation
+ let n = PendingTreatmentsQueueManager.shared.incrementAndGet()
+ let fail = PendingTreatmentsQueueManager.shared.failures()
+ let uid = "#\(id):\(n) (\(fail))"
+
+ var treatment : NightscoutTreatment?
+ switch(eventType) {
+ case .debug:
+ let cal = Calendar.current
+ let comps = cal.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond], from: date)
+ let microSeconds = lrint(Double(comps.nanosecond!)/1000)
+ // This hack is here to prevent de-duplication of events on insert
+ // in Nightscout. Everything else is logged much less per second, this might
+ // be logged more than once.
+ let formatted = String(format: "Debug.%06ld", microSeconds)
+ treatment = NightscoutTreatment(timestamp: date, enteredBy: author, notes: "\(note) \(uid)", eventType: formatted)
+ case .note:
+ treatment = NoteNightscoutTreatment(timestamp: date, enteredBy: author, notes: "\(note) \(uid)")
+ case .insulinChange:
+ treatment = NightscoutTreatment(timestamp: date, enteredBy: author, notes: "Automatically added: \(note) \(uid)", eventType: "Insulin Change")
+ case .siteChange:
+ treatment = NightscoutTreatment(timestamp: date, enteredBy: author, notes: "Automatically added: \(note) \(uid)", eventType: "Site Change")
+ case .batteryChange:
+ treatment = NightscoutTreatment(timestamp: date, enteredBy: author, notes: "Automatically added: \(note) \(uid)", eventType: "Pump Battery Change")
+ case .sensorStart:
+ treatment = NightscoutTreatment(timestamp: date, enteredBy: author, notes: "Automatically added: \(note) \(uid)", eventType: "Sensor Start")
+ case .sensorChange:
+ treatment = NightscoutTreatment(timestamp: date, enteredBy: author, notes: "Automatically added: \(note) \(uid)", eventType: "Sensor Change")
+ case .sensorStop:
+ treatment = NightscoutTreatment(timestamp: date, enteredBy: author, notes: "Automatically added: \(note) \(uid)", eventType: "Sensor Stop")
+ case .bgReceived:
+ let parts = note.split(separator: " ", maxSplits: 1)
+ let amount = Int(parts[0]) ?? 0
+ let comment = String(parts[1])
+ treatment = BGCheckNightscoutTreatment(
+ timestamp: date,
+ enteredBy: author,
+ glucose: amount,
+ glucoseType: .Meter,
+ units: .MGDL,
+ notes: "\(comment) \(uid)"
+ )
+
+ }
+ guard let finalTreatment = treatment else {
+ return
+ }
+ PendingTreatmentsQueueManager.shared.queue.async {
+ PendingTreatmentsQueueManager.shared.pending.append(finalTreatment)
+ // UserDefaults.standard.pendingTreatments.append(event)
+ self.uploadTreatments()
+ }
+ }
+
+ private func uploadTreatments() {
+ dispatchPrecondition(condition: .onQueue(PendingTreatmentsQueueManager.shared.queue))
+ let pendingTreatments = PendingTreatmentsQueueManager.shared.pending
+ PendingTreatmentsQueueManager.shared.pending = []
+ // NSLog("UPLOADING", pendingTreatments.count)
+ let uploadGroup = DispatchGroup()
+
+ uploadGroup.enter()
+ let uploadTreatments = pendingTreatments
+ self.delegate.loopDataManager(self, uploadTreatments: uploadTreatments) { (result) in
+ switch(result) {
+ case .success:
+ // for (treatment, id) in zip(uploadTreatments, ids) {
+ // NSLog("UPLOADING SUCCESS", id, treatment.dictionaryRepresentation)
+ // }
+ _ = 1
+ case .failure(let error):
+ switch(error) {
+ case LoopError.configurationError:
+ NSLog("UPLOADING ERROR Nightscout not configured")
+ default:
+ for treatment in uploadTreatments {
+ // NSLog("UPLOADING ERROR", error, treatment.dictionaryRepresentation)
+ PendingTreatmentsQueueManager.shared.pending.append(treatment)
+ PendingTreatmentsQueueManager.shared.recordFailure()
+ }
+ }
+ }
+ uploadGroup.leave()
+ }
+
+ uploadGroup.wait()
+ }
+
+ public func addNote(_ text: String) {
+ NSLog("addNote: \(text)")
+ addFakeEvent(.note, text)
+ }
+
+ public func addInternalNote(_ text: String) {
+ NSLog("addInternalNote: \(text)")
+ addFakeEvent(.debug, "INTERNAL \(text)")
+ }
+
+ // Do not use directly, use logger instead
+ public func addDebugNote(_ text: String) {
+ NSLog("addDebugNote: \(text)")
+ addFakeEvent(.debug, "DEBUG \(text)")
+ }
+
+ public func addInsulinChange(_ text: String) {
+ addFakeEvent(.insulinChange, text)
+ }
+
+ public func addSiteChange(_ text: String) {
+ addFakeEvent(.siteChange, text)
+ }
+
+ public func addBGReceived(bloodGlucose: Int, comment: String = "") {
+ addFakeEvent(.bgReceived, "\(bloodGlucose) \(comment)")
+ }
+
+ public func addBatteryChange(_ text: String) {
+ addFakeEvent(.batteryChange, text)
+ }
+
+ public func addSensorStart(_ date: Date, _ note: String) {
+ addFakeEvent(.sensorStart, note, date)
+ }
}
diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift
index 88101cb1b5..3e5688d751 100644
--- a/Loop/Extensions/NSUserDefaults.swift
+++ b/Loop/Extensions/NSUserDefaults.swift
@@ -10,6 +10,7 @@ import Foundation
import LoopKit
import InsulinKit
import MinimedKit
+import NightscoutUploadKit
import HealthKit
import RileyLinkKit
@@ -38,7 +39,7 @@ extension UserDefaults {
extension UserDefaults {
- private enum Key: String {
+ fileprivate enum Key: String {
case basalRateSchedule = "com.loudnate.Naterade.BasalRateSchedule"
case batteryChemistry = "com.loopkit.Loop.BatteryChemistry"
case cgmSettings = "com.loopkit.Loop.cgmSettings"
@@ -165,9 +166,11 @@ extension UserDefaults {
let settings = LoopSettings(
dosingEnabled: bool(forKey: "com.loudnate.Naterade.DosingEnabled"),
+ bolusEnabled: false,
glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
maximumBasalRatePerHour: maximumBasalRatePerHour,
maximumBolus: maximumBolus,
+ maximumInsulinOnBoard: nil,
suspendThreshold: suspendThreshold,
retrospectiveCorrectionEnabled: bool(forKey: "com.loudnate.Loop.RetrospectiveCorrectionEnabled")
)
@@ -321,3 +324,203 @@ extension UserDefaults {
}
}
+
+
+// PRIVATE MODIFICATIONS
+extension UserDefaults {
+
+ // Avoid polluting the original Key above.
+ fileprivate enum PrivateKey: String {
+ case minimumBasalRateSchedule = "com.loudnate.Loop.MinBasalRateSchedule"
+ case foodStats = "com.loopkit.Loop.foodStats"
+ case foodManagerNeedUpload = "com.loopkit.Loop.foodNeedUpload"
+ case pumpDetachedMode = "com.loopkit.Loop.pumpDetachedMode"
+ case lastUploadedNightscoutProfile = "com.loopkit.Loop.lastUploadedNightscoutProfile"
+ case pendingTreatments = "com.loopkit.Loop.pendingTreatments"
+ case G5SessionStartDate = "com.loopkit.Loop.G5SessionStartDate"
+ }
+
+ var minimumBasalRateSchedule: BasalRateSchedule? {
+ get {
+ if let rawValue = dictionary(forKey: PrivateKey.minimumBasalRateSchedule.rawValue) {
+ return BasalRateSchedule(rawValue: rawValue)
+ } else {
+ return nil
+ }
+ }
+ set {
+ set(newValue?.rawValue, forKey: PrivateKey.minimumBasalRateSchedule.rawValue)
+ }
+ }
+
+
+ var textDump : String {
+ return self.dictionaryRepresentation().debugDescription
+ }
+
+ var foodStats : [String: [String: Int]] {
+ get {
+ if let rawValue = dictionary(forKey: PrivateKey.foodStats.rawValue) {
+ var ret : [String: [String: Int]] = [:]
+ for raw in rawValue {
+ if let val = raw.value as? [String: Int] {
+ let key = raw.key
+ ret[key] = val
+ }
+ }
+ return ret
+ } else {
+ return [:]
+ }
+ }
+ set {
+ set(newValue, forKey: PrivateKey.foodStats.rawValue)
+ }
+ }
+
+ var foodManagerNeedUpload : [String] {
+ get {
+ return array(forKey: PrivateKey.foodManagerNeedUpload.rawValue) as? [String] ?? []
+ }
+ set {
+ set(newValue, forKey: PrivateKey.foodManagerNeedUpload.rawValue)
+ }
+ }
+
+ var pendingTreatments: [(type: Int, date: Date, note: String)] {
+ get {
+ var ret : [(type: Int, date: Date, note: String)] = []
+ for element in array(forKey: PrivateKey.pendingTreatments.rawValue) as? [[String:Any]] ?? [] {
+ guard let type = element["type"] as? Int, let date = element["date"] as? Date, let note = element["note"] as? String else {
+ NSLog("Cannot parse stored pendingTreatment \(element)")
+ continue
+ }
+ ret.append((type: type, date: date, note: note))
+ }
+ return ret
+ }
+ set {
+ var raw : [[String:Any]] = []
+ for value in newValue {
+ raw.append([
+ "type": value.type,
+ "date": value.date,
+ "note": value.note
+ ])
+ }
+ set(raw, forKey: PrivateKey.pendingTreatments.rawValue)
+ }
+ }
+
+ var pumpDetachedMode: Date? {
+ get {
+ let value = double(forKey: PrivateKey.pumpDetachedMode.rawValue)
+ if value > 0 {
+ return Date(timeIntervalSinceReferenceDate: value)
+ } else {
+ return nil
+ }
+ }
+ set {
+ if newValue == nil {
+ removeObject(forKey: PrivateKey.pumpDetachedMode.rawValue)
+ } else {
+ set(newValue?.timeIntervalSinceReferenceDate, forKey: PrivateKey.pumpDetachedMode.rawValue)
+ }
+ }
+ }
+
+ var G5SessionStartDate: Date? {
+ get {
+ let value = double(forKey: PrivateKey.G5SessionStartDate.rawValue)
+ if value > 0 {
+ return Date(timeIntervalSinceReferenceDate: value)
+ } else {
+ return nil
+ }
+ }
+ set {
+ if newValue == nil {
+ removeObject(forKey: PrivateKey.G5SessionStartDate.rawValue)
+ } else {
+ set(newValue?.timeIntervalSinceReferenceDate, forKey: PrivateKey.G5SessionStartDate.rawValue)
+ }
+ }
+ }
+
+ var lastUploadedNightscoutProfile: String {
+ get {
+ return string(forKey: PrivateKey.lastUploadedNightscoutProfile.rawValue) ?? "{}"
+ }
+ set {
+ set(newValue, forKey: PrivateKey.lastUploadedNightscoutProfile.rawValue)
+ }
+ }
+
+ func uploadProfile(uploader: NightscoutUploader, retry: Int = 0) {
+ NSLog("uploadProfile")
+ guard let glucoseTargetRangeSchedule = loopSettings?.glucoseTargetRangeSchedule,
+ let insulinSensitivitySchedule = insulinSensitivitySchedule,
+ let carbRatioSchedule = carbRatioSchedule,
+ let basalRateSchedule = basalRateSchedule
+
+ else {
+ NSLog("uploadProfile - missing data")
+
+ return
+ }
+ if retry > 5 {
+ NSLog("uploadProfile - too many retries")
+ return
+ }
+ var settings = loopSettings?.rawValue ?? [:]
+ settings["minBasal"] = minimumBasalRateSchedule?.rawValue
+ settings["pumpId"] = pumpSettings?.pumpID
+ settings["pumpRegion"] = pumpSettings?.pumpRegion.description
+ settings["cgmSource"] = cgm?.rawValue
+ var targets : [String:String] = [:]
+ for range in loopSettings?.glucoseTargetRangeSchedule?.overrideRanges ?? [:] {
+ targets[range.key.rawValue] = "\(range.value.minValue) - \(range.value.maxValue)"
+ }
+ settings["workoutTargets"] = targets
+ let profile = NightscoutProfile(
+ timestamp: Date(),
+ name: "Loop2",
+ rangeSchedule: glucoseTargetRangeSchedule,
+ sensitivity: insulinSensitivitySchedule,
+ carbs: carbRatioSchedule,
+ basal : basalRateSchedule,
+ timezone : TimeZone.current,
+ dia : (insulinModelSettings?.model.effectDuration ?? 0) / 3600,
+ settings : settings
+ )
+ guard let json = profile.json else {
+ NSLog("uploadProfile - could not generate json!")
+ return
+ }
+ print("+++++++++")
+ print(json)
+ print("+++++++++")
+ if json != lastUploadedNightscoutProfile {
+ uploader.uploadProfile(profile) { (result) in
+ switch result {
+ case .failure(let error):
+ NSLog("uploadProfile failed, try \(retry): \(error)")
+ // Try again with linear backoff, this is not great as updates afterwards
+ // can potentially be overwritten.
+ let retries = retry + 1
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(300 * retries) ) {
+ self.uploadProfile(uploader: uploader, retry: retries)
+ }
+ case .success(_):
+ NSLog("uploadProfile - success")
+ self.lastUploadedNightscoutProfile = json
+ }
+ }
+ } else {
+ NSLog("uploadProfile - no change!")
+ }
+ }
+
+}
+
diff --git a/Loop/Extensions/NightscoutUploader+Profile.swift b/Loop/Extensions/NightscoutUploader+Profile.swift
new file mode 100644
index 0000000000..34c5947189
--- /dev/null
+++ b/Loop/Extensions/NightscoutUploader+Profile.swift
@@ -0,0 +1,279 @@
+//
+// NightscoutUploader+Profile.swift
+// Loop2
+//
+// Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import LoopKit
+import NightscoutUploadKit
+
+private let defaultNightscoutProfilePath = "/api/v1/profile"
+
+class NightscoutTimeFormat: NSObject {
+ private static var formatterISO8601 : DateFormatter {
+ let formatter = DateFormatter()
+ formatter.calendar = Calendar(identifier: Calendar.Identifier.iso8601)
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"
+
+ return formatter
+ }
+
+ static func timestampStrFromDate(_ date: Date) -> String {
+ return formatterISO8601.string(from: date)
+ }
+}
+
+public class NightscoutProfile {
+
+ let timestamp : Date
+ let name : String
+ let rangeSchedule : GlucoseRangeSchedule
+ let sensitivity : InsulinSensitivitySchedule
+ let carbs : CarbRatioSchedule
+ let basal : BasalRateSchedule
+ let timezone : String
+ let dia : Double
+ let settings : [String:Any]
+
+ public init(timestamp: Date, name: String, rangeSchedule: GlucoseRangeSchedule,
+ sensitivity: InsulinSensitivitySchedule,
+ carbs: CarbRatioSchedule,
+ basal : BasalRateSchedule,
+ timezone : TimeZone,
+ dia : Double,
+ settings : [String:Any] = [:]
+ ) {
+ self.timestamp = timestamp
+ self.name = name
+ self.rangeSchedule = rangeSchedule
+ self.sensitivity = sensitivity
+ self.carbs = carbs
+ self.basal = basal
+ self.timezone = timezone.identifier
+ self.dia = dia
+ self.settings = settings
+ }
+
+ private func formatItem(_ time: TimeInterval, _ value: Any) -> [String:Any] {
+ let hours = Int(time / 3600)
+ let minutes = (time / 60).truncatingRemainder(dividingBy: 60)
+ var rep : [String: Any] = [:]
+ rep["time"] = String(format:"%02i:%02i", hours, minutes)
+ rep["value"] = value
+ rep["timeAsSeconds"] = Int(time)
+ return rep
+ }
+
+ public var json : String? {
+ do {
+ var dict = dictionaryRepresentation
+ dict["created_at"] = ""
+ dict["startDate"] = ""
+ let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted, .sortedKeys])
+ if let encodedData = String(data: data, encoding: .utf8) {
+ return encodedData
+ }
+ } catch (let error) {
+ NSLog("NightscoutProfile encoding to json error: \(error)")
+ }
+ return nil
+ }
+
+ public var dictionaryRepresentation: [String: Any] {
+ var profile : [String: Any] = [:]
+ profile["dia"] = self.dia
+ profile["carbs_hr"] = "0"
+ profile["delay"] = "0"
+
+ profile["timezone"] = timezone
+
+ var target_low = [[String:Any]]()
+ var target_high = [[String:Any]]()
+ for item in self.rangeSchedule.items {
+ target_low.append(formatItem(item.startTime, item.value.minValue))
+ target_high.append(formatItem(item.startTime, item.value.maxValue))
+ }
+ profile["target_low"] = target_low
+ profile["target_high"] = target_high
+
+ var sens = [[String:Any]]()
+ for item in self.sensitivity.items {
+ sens.append(formatItem(item.startTime, item.value))
+ }
+ profile["sens"] = sens
+
+ var basal = [[String:Any]]()
+ for item in self.basal.items {
+ basal.append(formatItem(item.startTime, item.value))
+ }
+ profile["basal"] = basal
+
+ var carbratio = [[String:Any]]()
+ for item in self.carbs.items {
+ carbratio.append(formatItem(item.startTime, item.value))
+ }
+ profile["carbratio"] = carbratio
+
+ var store : [String: Any] = [:]
+ let profileName = "Default"
+ store[profileName] = profile
+
+ var rval : [String: Any] = [:]
+
+ rval["defaultProfile"] = profileName
+ rval["mills"] = "0" // ?
+ rval["units"] = self.rangeSchedule.unit.glucoseUnitDisplayString
+ rval["startDate"] = NightscoutTimeFormat.timestampStrFromDate(timestamp)
+ rval["created_at"] = NightscoutTimeFormat.timestampStrFromDate(timestamp)
+ rval["enteredBy"] = "loop2"
+ rval["store"] = store
+ var settings = self.settings
+ settings.removeValue(forKey: "glucoseTargetRangeSchedule")
+ rval["loopSettings"] = settings
+ return rval
+ }
+}
+
+extension NightscoutUploader {
+
+ public func uploadProfile(_ profile: NightscoutProfile, completion: @escaping (Either<[String],Error>) -> Void) {
+ let inFlight = [profile]
+
+ profilePostToNS(inFlight.map({$0.dictionaryRepresentation}), endpoint: defaultNightscoutProfilePath) { (result) in
+ switch result {
+ case .failure(let error):
+ self.errorHandler?(error, "Uploading nightscout profile records")
+ // Requeue
+ //self.treatmentsQueue.append(contentsOf: inFlight)
+ case .success(_):
+ //if let last = inFlight.last {
+ // self.lastStoredTreatmentTimestamp = last.timestamp
+ //}
+ break
+ }
+ completion(result)
+ }
+ }
+
+ // Blunt copies but internal protection level makes them inaccessible
+ func profilePostToNS(_ json: [Any], endpoint:String, completion: @escaping (Either<[String],Error>) -> Void) {
+ if json.count == 0 {
+ completion(.success([]))
+ return
+ }
+
+ profileCallNS(json, endpoint: endpoint, method: "POST") { (result) in
+ switch result {
+ case .success(let json):
+ guard let insertedEntries = json as? [[String: Any]] else {
+ completion(.failure(UploadError.invalidResponse(reason: "Expected array of objects in JSON response")))
+ return
+ }
+
+ let ids = insertedEntries.map({ (entry: [String: Any]) -> String in
+ if let id = entry["_id"] as? String {
+ return id
+ } else {
+ // Upload still succeeded; likely that this is an old version of NS
+ // Instead of failing (which would cause retries later, we just mark
+ // This entry has having an id of 'NA', which will let us consider it
+ // uploaded.
+ //throw UploadError.invalidResponse(reason: "Invalid/missing id in response.")
+ return "NA"
+ }
+ })
+ completion(.success(ids))
+ case .failure(let error):
+ completion(.failure(error))
+ }
+
+ }
+ }
+
+ func profileCallNS(_ json: Any?, endpoint:String, method:String, completion: @escaping (Either) -> Void) {
+ let uploadURL = siteURL.appendingPathComponent(endpoint)
+ var request = URLRequest(url: uploadURL)
+ request.httpMethod = method
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+ request.setValue(apiSecret.sha1, forHTTPHeaderField: "api-secret")
+
+ do {
+
+ if let json = json {
+ let sendData = try JSONSerialization.data(withJSONObject: json, options: [])
+ let task = URLSession.shared.uploadTask(with: request, from: sendData, completionHandler: { (data, response, error) in
+ if let error = error {
+ completion(.failure(error))
+ return
+ }
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ completion(.failure(UploadError.invalidResponse(reason: "Response is not HTTPURLResponse")))
+ return
+ }
+
+ if httpResponse.statusCode != 200 {
+ let error = UploadError.httpError(status: httpResponse.statusCode, body:String(data: data!, encoding: String.Encoding.utf8)!)
+ completion(.failure(error))
+ return
+ }
+
+ guard let data = data else {
+ completion(.failure(UploadError.invalidResponse(reason: "No data in response")))
+ return
+ }
+
+ do {
+ let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions())
+ completion(.success(json))
+ } catch {
+ completion(.failure(error))
+ return
+ }
+ })
+ task.resume()
+ } else {
+ let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
+ if let error = error {
+ completion(.failure(error))
+ return
+ }
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ completion(.failure(UploadError.invalidResponse(reason: "Response is not HTTPURLResponse")))
+ return
+ }
+
+ if httpResponse.statusCode != 200 {
+ let error = UploadError.httpError(status: httpResponse.statusCode, body:String(data: data!, encoding: String.Encoding.utf8)!)
+ completion(.failure(error))
+ return
+ }
+
+ guard let data = data else {
+ completion(.failure(UploadError.invalidResponse(reason: "No data in response")))
+ return
+ }
+
+ do {
+ let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions())
+ completion(.success(json))
+ } catch {
+ completion(.failure(error))
+ return
+ }
+ })
+ task.resume()
+ }
+
+ } catch let error {
+ completion(.failure(error))
+ }
+ }
+
+}
diff --git a/Loop/Extensions/NightscoutUploader.swift b/Loop/Extensions/NightscoutUploader.swift
index bc913138f3..31513b05a2 100644
--- a/Loop/Extensions/NightscoutUploader.swift
+++ b/Loop/Extensions/NightscoutUploader.swift
@@ -63,17 +63,43 @@ extension NightscoutUploader {
var objectIDs = [NSManagedObjectID]()
var timestampedPumpEvents = [TimestampedHistoryEvent]()
+ var fakeEvents = [NightscoutTreatment]()
+ let author = "loop://\(UIDevice.current.name)"
+
for event in events {
objectIDs.append(event.objectID)
if let raw = event.raw, raw.count > 0, let type = MinimedKit.PumpEventType(rawValue: raw[0])?.eventType, let pumpEvent = type.init(availableData: raw, pumpModel: pumpModel) {
timestampedPumpEvents.append(TimestampedHistoryEvent(pumpEvent: pumpEvent, date: event.date))
+
+ // Handle Events not handled in NightscoutPumpEvents
+ switch pumpEvent {
+ case let rewind as RewindPumpEvent:
+ _ = rewind
+ let entry = NightscoutTreatment(timestamp: event.date, enteredBy: author, notes: "Automatically added", eventType: "Insulin Change")
+ fakeEvents.append(entry)
+ case let prime as PrimePumpEvent:
+ let programmedAmount = prime.dictionaryRepresentation["programmedAmount"] ?? 0
+ let amount = prime.dictionaryRepresentation["amount"] ?? 0
+
+ let primeType = prime.dictionaryRepresentation["primeType"] ?? ""
+
+ let entry = NightscoutTreatment(timestamp: event.date, enteredBy: author, notes: "Automatically added; Amount \(amount) Units, Programmed Amount \(programmedAmount) Units, Type \(primeType)",
+ eventType: "Site Change")
+ fakeEvents.append(entry)
+ case let alarm as PumpAlarmPumpEvent:
+ let note = "Pump Alarm \(alarm.alarmType)"
+ let entry = NightscoutTreatment(timestamp: event.date, enteredBy: author, notes: note, eventType: "Announcement")
+ fakeEvents.append(entry)
+ default:
+ break
+ }
}
}
- let nsEvents = NightscoutPumpEvents.translate(timestampedPumpEvents, eventSource: "loop://\(UIDevice.current.name)", includeCarbs: false)
+ let nsEvents = NightscoutPumpEvents.translate(timestampedPumpEvents, eventSource: author, includeCarbs: false)
- self.upload(nsEvents) { (result) in
+ self.upload(nsEvents + fakeEvents) { (result) in
switch result {
case .success( _):
completion(.success(objectIDs))
diff --git a/Loop/Extensions/UIAlertController.swift b/Loop/Extensions/UIAlertController.swift
index 0dd971ea6c..054d97b711 100644
--- a/Loop/Extensions/UIAlertController.swift
+++ b/Loop/Extensions/UIAlertController.swift
@@ -16,7 +16,7 @@ extension UIAlertController {
- parameter handler: A closure to execute when the sheet is dismissed after selection. The closure has a single argument:
- endDate: The date at which the user selected the workout to end
*/
- convenience init(workoutDurationSelectionHandler handler: @escaping (_ endDate: Date) -> Void) {
+ convenience init(workoutDurationSelectionHandler handler: @escaping (_ endDate: Date, _ disconnect: Bool) -> Void) {
self.init(
title: NSLocalizedString("Use Workout Glucose Targets", comment: "The title of the alert controller used to select a duration for workout targets"),
message: nil,
@@ -27,19 +27,25 @@ extension UIAlertController {
formatter.allowsFractionalUnits = false
formatter.unitsStyle = .full
- for interval in [1, 2].map({ TimeInterval(hours: $0) }) {
- let duration = NSLocalizedString("For %1$@", comment: "The format string used to describe a finite workout targets duration")
+ for interval in [1, 2, 4].map({ TimeInterval(hours: $0) }) {
+ let duration = NSLocalizedString("🏃⚽ For %1$@", comment: "The format string used to describe a finite workout targets duration")
addAction(UIAlertAction(title: String(format: duration, formatter.string(from: interval)!), style: .default) { _ in
- handler(Date(timeIntervalSinceNow: interval))
+ handler(Date(timeIntervalSinceNow: interval), false)
})
}
-
- let distantFuture = NSLocalizedString("Indefinitely", comment: "The title of a target alert action specifying an indefinitely long workout targets duration")
- addAction(UIAlertAction(title: distantFuture, style: .default) { _ in
- handler(Date.distantFuture)
- })
-
+
+
+// let distantFuture = NSLocalizedString("Indefinitely", comment: "The title of a target alert action specifying an indefinitely long workout targets duration")
+// addAction(UIAlertAction(title: distantFuture, style: .default) { _ in
+// handler(Date.distantFuture)
+// })
+
+ let action = UIAlertAction(title: String(format: "🏊 Disconnect Pump"), style: .default) { _ in
+ handler(Date(timeIntervalSinceNow: -1), true)
+ }
+ addAction(action)
+
let cancel = NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet")
addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil))
}
diff --git a/Loop/FoodCatalog/Apfelmus.jpg b/Loop/FoodCatalog/Apfelmus.jpg
new file mode 100644
index 0000000000..2b51c3c915
Binary files /dev/null and b/Loop/FoodCatalog/Apfelmus.jpg differ
diff --git a/Loop/FoodCatalog/Apple.jpg b/Loop/FoodCatalog/Apple.jpg
new file mode 100644
index 0000000000..4c5a6c8a68
Binary files /dev/null and b/Loop/FoodCatalog/Apple.jpg differ
diff --git a/Loop/FoodCatalog/Banana.jpg b/Loop/FoodCatalog/Banana.jpg
new file mode 100644
index 0000000000..0571ef4ff6
Binary files /dev/null and b/Loop/FoodCatalog/Banana.jpg differ
diff --git a/Loop/FoodCatalog/Bratkartoffeln.jpg b/Loop/FoodCatalog/Bratkartoffeln.jpg
new file mode 100644
index 0000000000..561e46aee9
Binary files /dev/null and b/Loop/FoodCatalog/Bratkartoffeln.jpg differ
diff --git a/Loop/FoodCatalog/BreadSlice.jpg b/Loop/FoodCatalog/BreadSlice.jpg
new file mode 100644
index 0000000000..1d9bc0559b
Binary files /dev/null and b/Loop/FoodCatalog/BreadSlice.jpg differ
diff --git a/Loop/FoodCatalog/Brezel.jpg b/Loop/FoodCatalog/Brezel.jpg
new file mode 100644
index 0000000000..65b9b26e8e
Binary files /dev/null and b/Loop/FoodCatalog/Brezel.jpg differ
diff --git a/Loop/FoodCatalog/Brotscheibe.jpg b/Loop/FoodCatalog/Brotscheibe.jpg
new file mode 100644
index 0000000000..16a96a68cc
Binary files /dev/null and b/Loop/FoodCatalog/Brotscheibe.jpg differ
diff --git a/Loop/FoodCatalog/ChiliConCarne.jpg b/Loop/FoodCatalog/ChiliConCarne.jpg
new file mode 100644
index 0000000000..a268141965
Binary files /dev/null and b/Loop/FoodCatalog/ChiliConCarne.jpg differ
diff --git a/Loop/FoodCatalog/Clementine.jpg b/Loop/FoodCatalog/Clementine.jpg
new file mode 100644
index 0000000000..72288d8b36
Binary files /dev/null and b/Loop/FoodCatalog/Clementine.jpg differ
diff --git a/Loop/FoodCatalog/CrepeNutella.jpg b/Loop/FoodCatalog/CrepeNutella.jpg
new file mode 100644
index 0000000000..c458d80c0d
Binary files /dev/null and b/Loop/FoodCatalog/CrepeNutella.jpg differ
diff --git a/Loop/FoodCatalog/Croissant.jpg b/Loop/FoodCatalog/Croissant.jpg
new file mode 100644
index 0000000000..8324ba6208
Binary files /dev/null and b/Loop/FoodCatalog/Croissant.jpg differ
diff --git a/Loop/FoodCatalog/Duplo.jpg b/Loop/FoodCatalog/Duplo.jpg
new file mode 100644
index 0000000000..931d883825
Binary files /dev/null and b/Loop/FoodCatalog/Duplo.jpg differ
diff --git a/Loop/FoodCatalog/Ei.jpg b/Loop/FoodCatalog/Ei.jpg
new file mode 100644
index 0000000000..342ffd5afd
Binary files /dev/null and b/Loop/FoodCatalog/Ei.jpg differ
diff --git a/Loop/FoodCatalog/Fishsticks.jpg b/Loop/FoodCatalog/Fishsticks.jpg
new file mode 100644
index 0000000000..5773e8283f
Binary files /dev/null and b/Loop/FoodCatalog/Fishsticks.jpg differ
diff --git a/Loop/FoodCatalog/FrecheFreunde.jpg b/Loop/FoodCatalog/FrecheFreunde.jpg
new file mode 100644
index 0000000000..9d43014a40
Binary files /dev/null and b/Loop/FoodCatalog/FrecheFreunde.jpg differ
diff --git a/Loop/FoodCatalog/FrenchFries.jpg b/Loop/FoodCatalog/FrenchFries.jpg
new file mode 100644
index 0000000000..9ece8bb4b7
Binary files /dev/null and b/Loop/FoodCatalog/FrenchFries.jpg differ
diff --git a/Loop/FoodCatalog/Fruchtjoghurt.jpg b/Loop/FoodCatalog/Fruchtjoghurt.jpg
new file mode 100644
index 0000000000..5d00d45b62
Binary files /dev/null and b/Loop/FoodCatalog/Fruchtjoghurt.jpg differ
diff --git a/Loop/FoodCatalog/GlucoTabs.jpg b/Loop/FoodCatalog/GlucoTabs.jpg
new file mode 100644
index 0000000000..19d3c5d329
Binary files /dev/null and b/Loop/FoodCatalog/GlucoTabs.jpg differ
diff --git a/Loop/FoodCatalog/Grapes.jpg b/Loop/FoodCatalog/Grapes.jpg
new file mode 100644
index 0000000000..8e6f3283b9
Binary files /dev/null and b/Loop/FoodCatalog/Grapes.jpg differ
diff --git a/Loop/FoodCatalog/GummibearsSmall.jpg b/Loop/FoodCatalog/GummibearsSmall.jpg
new file mode 100644
index 0000000000..fa74b4b5cc
Binary files /dev/null and b/Loop/FoodCatalog/GummibearsSmall.jpg differ
diff --git a/Loop/FoodCatalog/HalbeSemmel.jpg b/Loop/FoodCatalog/HalbeSemmel.jpg
new file mode 100644
index 0000000000..1f1016d25b
Binary files /dev/null and b/Loop/FoodCatalog/HalbeSemmel.jpg differ
diff --git a/Loop/FoodCatalog/Hanuta.jpg b/Loop/FoodCatalog/Hanuta.jpg
new file mode 100644
index 0000000000..b144d88c42
Binary files /dev/null and b/Loop/FoodCatalog/Hanuta.jpg differ
diff --git a/Loop/FoodCatalog/Juice.jpg b/Loop/FoodCatalog/Juice.jpg
new file mode 100644
index 0000000000..c3ae150e56
Binary files /dev/null and b/Loop/FoodCatalog/Juice.jpg differ
diff --git a/Loop/FoodCatalog/Kaiserschmarrn.jpg b/Loop/FoodCatalog/Kaiserschmarrn.jpg
new file mode 100644
index 0000000000..f5388f6482
Binary files /dev/null and b/Loop/FoodCatalog/Kaiserschmarrn.jpg differ
diff --git a/Loop/FoodCatalog/Kakao.jpg b/Loop/FoodCatalog/Kakao.jpg
new file mode 100644
index 0000000000..85e4974925
Binary files /dev/null and b/Loop/FoodCatalog/Kakao.jpg differ
diff --git a/Loop/FoodCatalog/Kartoffelbrei.jpg b/Loop/FoodCatalog/Kartoffelbrei.jpg
new file mode 100644
index 0000000000..8735111c21
Binary files /dev/null and b/Loop/FoodCatalog/Kartoffelbrei.jpg differ
diff --git a/Loop/FoodCatalog/Kartoffelpuffer.jpg b/Loop/FoodCatalog/Kartoffelpuffer.jpg
new file mode 100644
index 0000000000..f5dd11a2d4
Binary files /dev/null and b/Loop/FoodCatalog/Kartoffelpuffer.jpg differ
diff --git a/Loop/FoodCatalog/Keks.jpg b/Loop/FoodCatalog/Keks.jpg
new file mode 100644
index 0000000000..34862ae452
Binary files /dev/null and b/Loop/FoodCatalog/Keks.jpg differ
diff --git a/Loop/FoodCatalog/KinderRiegel.jpg b/Loop/FoodCatalog/KinderRiegel.jpg
new file mode 100644
index 0000000000..87a2c6918f
Binary files /dev/null and b/Loop/FoodCatalog/KinderRiegel.jpg differ
diff --git a/Loop/FoodCatalog/Kirschen.jpg b/Loop/FoodCatalog/Kirschen.jpg
new file mode 100644
index 0000000000..608ec1bba5
Binary files /dev/null and b/Loop/FoodCatalog/Kirschen.jpg differ
diff --git a/Loop/FoodCatalog/Knoppers.jpg b/Loop/FoodCatalog/Knoppers.jpg
new file mode 100644
index 0000000000..be7ecc5c71
Binary files /dev/null and b/Loop/FoodCatalog/Knoppers.jpg differ
diff --git a/Loop/FoodCatalog/Kuchen.jpg b/Loop/FoodCatalog/Kuchen.jpg
new file mode 100644
index 0000000000..5521bb76de
Binary files /dev/null and b/Loop/FoodCatalog/Kuchen.jpg differ
diff --git a/Loop/FoodCatalog/Lasagne.jpg b/Loop/FoodCatalog/Lasagne.jpg
new file mode 100644
index 0000000000..76539fb1b9
Binary files /dev/null and b/Loop/FoodCatalog/Lasagne.jpg differ
diff --git a/Loop/FoodCatalog/Milk.jpg b/Loop/FoodCatalog/Milk.jpg
new file mode 100644
index 0000000000..df400e89ec
Binary files /dev/null and b/Loop/FoodCatalog/Milk.jpg differ
diff --git a/Loop/FoodCatalog/Muesli.jpg b/Loop/FoodCatalog/Muesli.jpg
new file mode 100644
index 0000000000..0da7ae5a44
Binary files /dev/null and b/Loop/FoodCatalog/Muesli.jpg differ
diff --git a/Loop/FoodCatalog/Nuggets.jpg b/Loop/FoodCatalog/Nuggets.jpg
new file mode 100644
index 0000000000..af286715dc
Binary files /dev/null and b/Loop/FoodCatalog/Nuggets.jpg differ
diff --git a/Loop/FoodCatalog/NutellaCroissant.jpg b/Loop/FoodCatalog/NutellaCroissant.jpg
new file mode 100644
index 0000000000..c40eaeaaf3
Binary files /dev/null and b/Loop/FoodCatalog/NutellaCroissant.jpg differ
diff --git a/Loop/FoodCatalog/Pasta.jpg b/Loop/FoodCatalog/Pasta.jpg
new file mode 100644
index 0000000000..6aab662ab3
Binary files /dev/null and b/Loop/FoodCatalog/Pasta.jpg differ
diff --git a/Loop/FoodCatalog/Photo.jpg b/Loop/FoodCatalog/Photo.jpg
new file mode 100644
index 0000000000..4bb678061f
Binary files /dev/null and b/Loop/FoodCatalog/Photo.jpg differ
diff --git a/Loop/FoodCatalog/Pizza.jpg b/Loop/FoodCatalog/Pizza.jpg
new file mode 100644
index 0000000000..b2422c1b98
Binary files /dev/null and b/Loop/FoodCatalog/Pizza.jpg differ
diff --git a/Loop/FoodCatalog/Potatoes.jpg b/Loop/FoodCatalog/Potatoes.jpg
new file mode 100644
index 0000000000..8ec4e6fd70
Binary files /dev/null and b/Loop/FoodCatalog/Potatoes.jpg differ
diff --git a/Loop/FoodCatalog/Reiswaffel.jpg b/Loop/FoodCatalog/Reiswaffel.jpg
new file mode 100644
index 0000000000..e0080ff874
Binary files /dev/null and b/Loop/FoodCatalog/Reiswaffel.jpg differ
diff --git a/Loop/FoodCatalog/Rice.jpg b/Loop/FoodCatalog/Rice.jpg
new file mode 100644
index 0000000000..bc024b7b55
Binary files /dev/null and b/Loop/FoodCatalog/Rice.jpg differ
diff --git a/Loop/FoodCatalog/Schorle.jpg b/Loop/FoodCatalog/Schorle.jpg
new file mode 100644
index 0000000000..e38445b0e1
Binary files /dev/null and b/Loop/FoodCatalog/Schorle.jpg differ
diff --git a/Loop/FoodCatalog/Semmel.jpg b/Loop/FoodCatalog/Semmel.jpg
new file mode 100644
index 0000000000..a5baa4a33d
Binary files /dev/null and b/Loop/FoodCatalog/Semmel.jpg differ
diff --git a/Loop/FoodCatalog/Smarties-Mini.jpg b/Loop/FoodCatalog/Smarties-Mini.jpg
new file mode 100644
index 0000000000..b662afab57
Binary files /dev/null and b/Loop/FoodCatalog/Smarties-Mini.jpg differ
diff --git a/Loop/FoodCatalog/Spaetzle.jpg b/Loop/FoodCatalog/Spaetzle.jpg
new file mode 100644
index 0000000000..943affac77
Binary files /dev/null and b/Loop/FoodCatalog/Spaetzle.jpg differ
diff --git a/Loop/FoodCatalog/Squetch.jpg b/Loop/FoodCatalog/Squetch.jpg
new file mode 100644
index 0000000000..8e7e1ecace
Binary files /dev/null and b/Loop/FoodCatalog/Squetch.jpg differ
diff --git a/Loop/FoodCatalog/Tomatensuppe.jpg b/Loop/FoodCatalog/Tomatensuppe.jpg
new file mode 100644
index 0000000000..7f8ce1c73a
Binary files /dev/null and b/Loop/FoodCatalog/Tomatensuppe.jpg differ
diff --git a/Loop/FoodCatalog/Watermelon.jpg b/Loop/FoodCatalog/Watermelon.jpg
new file mode 100644
index 0000000000..5e230e4232
Binary files /dev/null and b/Loop/FoodCatalog/Watermelon.jpg differ
diff --git a/Loop/FoodCatalog/catalog.json b/Loop/FoodCatalog/catalog.json
new file mode 100644
index 0000000000..4c12c62403
--- /dev/null
+++ b/Loop/FoodCatalog/catalog.json
@@ -0,0 +1,386 @@
+{
+ "Apple": {
+ "categories": ["Fruit"],
+ "portion": 130,
+ "ratio": 14,
+ "image": null,
+ "title": null,
+ "type": "single"
+ },
+
+ "Banana": {
+ "categories": ["Fruit"],
+ "portion": 120,
+ "ratio": 22,
+ "type": "single"
+ },
+
+ "Weintraube": {
+ "categories": ["Fruit"],
+ "portion": 4,
+ "ratio": 16,
+ "initial": 5,
+ "image": "Grapes",
+ "type": "multiple"
+ },
+
+ "Kirschen": {
+ "categories": ["Fruit"],
+ "portion": 10,
+ "ratio": 14,
+ "initial": 3,
+ "image": "Kirschen",
+ "type": "multiple"
+ },
+
+ "◔ Wassermelone": {
+ "categories": ["Fruit"],
+ "portion": 50,
+ "ratio": 8,
+ "initial": 3,
+ "absorption": "fast",
+ "image": "Watermelon",
+ "type": "multiple"
+ },
+
+
+ "Kakao": {
+ "categories": ["Drinks"],
+ "portion": 250,
+ "ratio": 8,
+ "type": "drink"
+ },
+
+ "Milch": {
+ "categories": ["Drinks"],
+ "portion": 250,
+ "ratio": 5,
+ "image": "Milk",
+ "type": "drink"
+ },
+
+ "Saft": {
+ "categories": ["Drinks"],
+ "portion": 250,
+ "ratio": 10,
+ "absorption": "fast",
+ "type": "drink",
+ "image": "Juice"
+ },
+
+ "Schorle": {
+ "categories": ["Drinks"],
+ "portion": 250,
+ "ratio": 6,
+ "absorption": "fast",
+ "type": "drink"
+ },
+
+ "Fishsticks": {
+ "categories": ["Lunch"],
+ "portion": 30,
+ "ratio": 18,
+ "initial": 3,
+ "type": "multiple"
+ },
+
+ "Rice": {
+ "categories": ["Lunch"],
+ "portion": 100,
+ "ratio": 27,
+ "type": "continuous"
+ },
+
+ "Pasta": {
+ "categories": ["Lunch"],
+ "portion": 200,
+ "ratio": 25,
+ "absorption": "slow",
+ "type": "continuous"
+ },
+
+ "Lasagne": {
+ "categories": ["Lunch"],
+ "portion": 250,
+ "ratio": 9,
+ "absorption": "slow",
+ "type": "continuous"
+ },
+
+ "Nuggets": {
+ "categories": ["Lunch"],
+ "portion": 18,
+ "ratio": 15,
+ "initial": 1,
+ "type": "multiple"
+ },
+
+ "Reiswaffel": {
+ "categories": ["Snack"],
+ "portion": 7,
+ "ratio": 81,
+ "initial": 1,
+ "type": "single"
+ },
+
+ "Tomatensuppe": {
+ "categories": ["Lunch"],
+ "portion": 200,
+ "ratio": 7,
+ "type": "continuous"
+ },
+
+ "2 Kartoffeln": {
+ "info": "ca. 2 Kartoffeln",
+ "image": "kartoffeln_zwei",
+ "categories": ["Lunch"],
+ "portion": 160,
+ "ratio": 15,
+ "type": "continuous"
+ },
+
+ "Kartoffelbrei": {
+ "categories": ["Lunch"],
+ "portion": 200,
+ "ratio": 16,
+ "type": "continuous"
+ },
+
+ "Bratkartoffeln": {
+ "categories": ["Lunch"],
+ "portion": 200,
+ "ratio": 15,
+ "type": "continuous"
+ },
+
+ "Kartoffelpuffer": {
+ "categories": ["Lunch"],
+ "portion": 60,
+ "ratio": 25,
+ "type": "multiple"
+ },
+
+ "Pommes": {
+ "image": "FrenchFries",
+ "categories": ["Lunch"],
+ "portion": 180,
+ "ratio": 37,
+ "type": "continuous"
+ },
+
+ "Pizza Slice": {
+ "image": "Pizza",
+ "info": "ca. 1/4 25cm Pizza",
+ "categories": ["Lunch"],
+ "portion": 80,
+ "ratio": 25,
+ "absorption": "slow",
+ "type": "multiple"
+ },
+
+ "Kaiserschmarrn": {
+ "image": "Kaiserschmarrn",
+ "categories": ["Lunch"],
+ "portion": 200,
+ "ratio": 22,
+ "type": "single"
+ },
+
+ "Apfelmus": {
+ "categories": ["Lunch", "Snack"],
+ "portion": 150,
+ "ratio": 15,
+ "type": "continuous"
+ },
+
+ "Spaetzle": {
+ "image": "Spaetzle",
+ "categories": ["Lunch"],
+ "portion": 200,
+ "ratio": 34,
+ "type": "single"
+ },
+
+ "Chili con Carne": {
+ "image": "ChiliConCarne",
+ "categories": ["Lunch"],
+ "portion": 250,
+ "ratio": 18,
+ "type": "single"
+ },
+
+ "Muesli, klein": {
+ "categories": ["Breakfast"],
+ "image": "Muesli",
+ "comment": "35g Muesli = 17g, 100g Milch = 6g",
+ "portion": 140,
+ "ratio": 18,
+ "absorption": "fast",
+ "type": "single"
+ },
+
+ "Hoernchen": {
+ "categories": ["Bakery"],
+ "image": "schaer_croissant",
+ "portion": 55,
+ "ratio": 51,
+ "type": "single"
+ },
+
+ "Bon Matin": {
+ "categories": ["Bakery"],
+ "image": "schaer_bon_matin",
+ "portion": 50,
+ "ratio": 50,
+ "type": "single"
+ },
+
+ "Nutella Portion": {
+ "categories": ["Bakery"],
+ "image": "nutella_klein",
+ "portion": 6,
+ "ratio": 57,
+ "type": "single"
+ },
+
+ "Ciabatta": {
+ "categories": ["Bakery"],
+ "image": "schaer_ciabatta",
+ "portion": 50,
+ "ratio": 46,
+ "type": "single"
+ },
+
+ "Hamburger": {
+ "categories": ["Bakery"],
+ "image": "schaer_hamburger",
+ "portion": 75,
+ "ratio": 45,
+ "type": "single"
+ },
+
+ "Weissbrot": {
+ "categories": ["Bakery"],
+ "image": "schaer_weissbrot",
+ "portion": 27,
+ "ratio": 45,
+ "type": "single"
+ },
+
+ "Landbrot": {
+ "categories": ["Bakery"],
+ "image": "schaer_landbrot",
+ "portion": 30,
+ "ratio": 40,
+ "type": "single"
+ },
+
+ "KinderRiegel": {
+ "categories": ["Sweets"],
+ "portion": 13,
+ "ratio": 54,
+ "absorption": "fast",
+ "type": "multiple"
+ },
+
+ "GlucoTabs": {
+ "categories": ["Sweets"],
+ "portion": 4,
+ "ratio": 100,
+ "absorption": "ultrafast",
+ "type": "multiple"
+ },
+
+ "FrecheFreunde": {
+ "categories": ["Snack"],
+ "portion": 100,
+ "ratio": 15,
+ "type": "multiple"
+ },
+
+ "Squetch": {
+ "categories": ["Snack"],
+ "portion": 90,
+ "ratio": 13,
+ "type": "multiple"
+ },
+
+ "Gummibaerchen": {
+ "categories": ["Sweets"],
+ "image": "GummibearsSmall",
+ "portion": 2,
+ "ratio": 77,
+ "initial": 3,
+ "absorption": "fast",
+ "type": "multiple"
+ },
+
+ "Brezel": {
+ "categories": ["Bakery", "Lunch"],
+ "portion": 80,
+ "ratio": 57,
+ "type": "single"
+ },
+
+ "Clementine": {
+ "categories": ["Fruit"],
+ "portion": 50,
+ "ratio": 12,
+ "initial": 1,
+ "type": "multiple"
+ },
+
+ "Apfelsine": {
+ "categories": ["Fruit"],
+ "image": "orange",
+ "portion": 130,
+ "ratio": 9,
+ "initial": 1,
+ "type": "single"
+ },
+
+ "Keks": {
+ "categories": ["Sweets"],
+ "portion": 20,
+ "ratio": 56,
+ "type": "single"
+ },
+
+ "Kuchen": {
+ "categories": ["Sweets"],
+ "portion": 70,
+ "ratio": 50,
+ "type": "single"
+ },
+
+ "M&M'S Choco": {
+ "categories": ["Sweets"],
+ "image": "m-m_choco",
+ "portion": 45,
+ "ratio": 69,
+ "type": "single"
+ },
+
+ "M&M'S Peanut": {
+ "categories": ["Sweets"],
+ "image": "m-m_peanut",
+ "portion": 45,
+ "ratio": 59,
+ "type": "single"
+ },
+
+ "Fruchtjoghurt": {
+ "categories": ["Snack"],
+ "portion": 150,
+ "ratio": 15,
+ "type": "continuous"
+ },
+
+ "Custom": {
+ "categories": ["Custom", "Popular"],
+ "portion": 20,
+ "ratio": 100,
+ "type": "continuous"
+ },
+
+}
diff --git a/Loop/FoodCatalog/kartoffeln_zwei.jpg b/Loop/FoodCatalog/kartoffeln_zwei.jpg
new file mode 100644
index 0000000000..9014c56061
Binary files /dev/null and b/Loop/FoodCatalog/kartoffeln_zwei.jpg differ
diff --git a/Loop/FoodCatalog/knoedel.jpg b/Loop/FoodCatalog/knoedel.jpg
new file mode 100644
index 0000000000..a6a02639c1
Binary files /dev/null and b/Loop/FoodCatalog/knoedel.jpg differ
diff --git a/Loop/FoodCatalog/m-m_choco.jpg b/Loop/FoodCatalog/m-m_choco.jpg
new file mode 100644
index 0000000000..57a07853d0
Binary files /dev/null and b/Loop/FoodCatalog/m-m_choco.jpg differ
diff --git a/Loop/FoodCatalog/m-m_peanut.jpg b/Loop/FoodCatalog/m-m_peanut.jpg
new file mode 100644
index 0000000000..5eb462dfcf
Binary files /dev/null and b/Loop/FoodCatalog/m-m_peanut.jpg differ
diff --git a/Loop/FoodCatalog/nutella_klein.jpg b/Loop/FoodCatalog/nutella_klein.jpg
new file mode 100644
index 0000000000..3f25beda21
Binary files /dev/null and b/Loop/FoodCatalog/nutella_klein.jpg differ
diff --git a/Loop/FoodCatalog/orange.jpg b/Loop/FoodCatalog/orange.jpg
new file mode 100644
index 0000000000..5358f153fc
Binary files /dev/null and b/Loop/FoodCatalog/orange.jpg differ
diff --git a/Loop/FoodCatalog/schaer_bon_matin.jpg b/Loop/FoodCatalog/schaer_bon_matin.jpg
new file mode 100644
index 0000000000..8ee664426c
Binary files /dev/null and b/Loop/FoodCatalog/schaer_bon_matin.jpg differ
diff --git a/Loop/FoodCatalog/schaer_ciabatta.jpg b/Loop/FoodCatalog/schaer_ciabatta.jpg
new file mode 100644
index 0000000000..67389bf24a
Binary files /dev/null and b/Loop/FoodCatalog/schaer_ciabatta.jpg differ
diff --git a/Loop/FoodCatalog/schaer_croissant.jpg b/Loop/FoodCatalog/schaer_croissant.jpg
new file mode 100644
index 0000000000..904471bec3
Binary files /dev/null and b/Loop/FoodCatalog/schaer_croissant.jpg differ
diff --git a/Loop/FoodCatalog/schaer_hamburger.jpg b/Loop/FoodCatalog/schaer_hamburger.jpg
new file mode 100644
index 0000000000..dbfa3a968f
Binary files /dev/null and b/Loop/FoodCatalog/schaer_hamburger.jpg differ
diff --git a/Loop/FoodCatalog/schaer_landbrot.jpg b/Loop/FoodCatalog/schaer_landbrot.jpg
new file mode 100644
index 0000000000..e2e6e1ff8e
Binary files /dev/null and b/Loop/FoodCatalog/schaer_landbrot.jpg differ
diff --git a/Loop/FoodCatalog/schaer_weissbrot.jpg b/Loop/FoodCatalog/schaer_weissbrot.jpg
new file mode 100644
index 0000000000..d638eb7b4b
Binary files /dev/null and b/Loop/FoodCatalog/schaer_weissbrot.jpg differ
diff --git a/Loop/Info.plist b/Loop/Info.plist
index e318f38873..1ef852c4bb 100644
--- a/Loop/Info.plist
+++ b/Loop/Info.plist
@@ -7,7 +7,7 @@
CFBundleDevelopmentRegion
en
CFBundleDisplayName
- Loop
+ Loop2
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.5.6
+ 1.9.4.20200329
CFBundleSignature
????
CFBundleURLTypes
@@ -45,18 +45,26 @@
NSBluetoothPeripheralUsageDescription
Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices
+ NSCameraUsageDescription
+ Taking pictures of Food
+ NSFaceIDUsageDescription
+ Face ID is used to authenticate insulin bolus.
NSHealthShareUsageDescription
Meal data from the Health database is used to determine glucose effects.
Glucose data from the Health database is used for graphing and momentum calculation.
NSHealthUpdateUsageDescription
Carbohydrate meal data entered in the app and on the watch is stored in the Health database.
Glucose data retrieved from the CGM is stored securely in HealthKit.
- NSFaceIDUsageDescription
- Face ID is used to authenticate insulin bolus.
+ NSPhotoLibraryUsageDescription
+ Storing pictures of Food
UIBackgroundModes
+ location
+ fetch
bluetooth-central
+ UIFileSharingEnabled
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
diff --git a/Loop/Loop.entitlements b/Loop/Loop.entitlements
index 8d88cb3139..ac650173e5 100644
--- a/Loop/Loop.entitlements
+++ b/Loop/Loop.entitlements
@@ -6,7 +6,7 @@
com.apple.security.application-groups
- $(APP_GROUP_IDENTIFIER)
+ group.net.loopkit.loop2.LoopGroup
diff --git a/Loop/Managers/AnalyticsManager.swift b/Loop/Managers/AnalyticsManager.swift
index fe08839154..ae09bad567 100644
--- a/Loop/Managers/AnalyticsManager.swift
+++ b/Loop/Managers/AnalyticsManager.swift
@@ -29,7 +29,9 @@ final class AnalyticsManager: IdentifiableClass {
}
static let shared = AnalyticsManager()
-
+
+ public var loopManager : LoopDataManager? = nil
+
// MARK: - Helpers
private var logger: CategoryLogger?
@@ -37,6 +39,12 @@ final class AnalyticsManager: IdentifiableClass {
private func logEvent(_ name: String, withProperties properties: [AnyHashable: Any]? = nil, outOfSession: Bool = false) {
logger?.debug("\(name) \(properties ?? [:])")
amplitudeService.client?.logEvent(name, withEventProperties: properties, outOfSession: outOfSession)
+
+ if let loop = self.loopManager {
+ if name != "Loop success" && name != "Status Screen" && name != "CGM Fetch" {
+ loop.addInternalNote("Analytics: \(name) \(properties ?? [:])")
+ }
+ }
}
// MARK: - UIApplicationDelegate
@@ -58,7 +66,9 @@ final class AnalyticsManager: IdentifiableClass {
func didDisplayStatusScreen() {
logEvent("Status Screen")
}
+
+
// MARK: - Config Events
func didChangeRileyLinkConnectionState() {
@@ -106,7 +116,10 @@ final class AnalyticsManager: IdentifiableClass {
}
func didChangeLoopSettings(from oldValue: LoopSettings, to newValue: LoopSettings) {
- logEvent("Loop settings change", outOfSession: true)
+ if oldValue.rawValue.debugDescription == newValue.rawValue.debugDescription {
+ return
+ }
+ logEvent("Loop settings change \(oldValue.rawValue.debugDescription) \(newValue.rawValue.debugDescription)", outOfSession: true)
if newValue.maximumBasalRatePerHour != oldValue.maximumBasalRatePerHour {
logEvent("Maximum basal rate change")
@@ -155,3 +168,25 @@ final class AnalyticsManager: IdentifiableClass {
logEvent("Loop error", outOfSession: true)
}
}
+
+// PRIVATE MODIFICATIONS
+extension AnalyticsManager {
+ func didDisplayQuickCarbScreen() {
+ logEvent("QuickCarb Screen")
+ }
+
+ func didDisplayFoodPicker() {
+ logEvent("QuickCarb Screen")
+ }
+
+ func didAddCarbsFromQuickCarbs(_ carbs: Int, _ glucose: Int, _ note: String) {
+ logEvent("AddCarbsFromQuickCarbs \(carbs)g \(glucose) mg/dl: \(note)")
+ }
+
+ func didAddCarbsFromFoodPicker(_ pick: FoodPick) {
+ logEvent("AddCarbsFromFoodPicker \(pick.item.title): \(pick.displayCarbs)g ")
+ }
+ func loopDidError(_ error: Error) {
+ logEvent("Loop error \(error.localizedDescription)", outOfSession: true)
+ }
+}
diff --git a/Loop/Managers/Bluetooth/Bluetooth-Bridging-Header.h b/Loop/Managers/Bluetooth/Bluetooth-Bridging-Header.h
new file mode 100644
index 0000000000..0c593462c7
--- /dev/null
+++ b/Loop/Managers/Bluetooth/Bluetooth-Bridging-Header.h
@@ -0,0 +1,28 @@
+
+/*
+ This file is part of BeeTee Project. It is subject to the license terms in the LICENSE file found in the top-level directory of this distribution and at https://github.com/michaeldorner/BeeTee/blob/master/LICENSE. No part of BeeTee Project, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the LICENSE file.
+
+ The MIT License (MIT)
+
+ Copyright (c) 2016 Michael Dorner
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+#import "BluetoothManagerHandler.h"
diff --git a/Loop/Managers/Bluetooth/BluetoothManager.h b/Loop/Managers/Bluetooth/BluetoothManager.h
new file mode 100644
index 0000000000..280b80cfda
--- /dev/null
+++ b/Loop/Managers/Bluetooth/BluetoothManager.h
@@ -0,0 +1,93 @@
+/* Generated by RuntimeBrowser
+ Image: /System/Library/PrivateFrameworks/BluetoothManager.framework/BluetoothManager
+ */
+
+@interface BluetoothManager : NSObject {
+ struct BTAccessoryManagerImpl { } * _accessoryManager;
+ BOOL _audioConnected;
+ int _available;
+ NSMutableDictionary * _btAddrDict;
+ NSMutableDictionary * _btDeviceDict;
+ struct BTDiscoveryAgentImpl { } * _discoveryAgent;
+ struct BTLocalDeviceImpl { } * _localDevice;
+ struct BTPairingAgentImpl { } * _pairingAgent;
+ BOOL _scanningEnabled;
+ BOOL _scanningInProgress;
+ unsigned int _scanningServiceMask;
+ struct BTSessionImpl { } * _session;
+}
+
+ // Image: /System/Library/PrivateFrameworks/BluetoothManager.framework/BluetoothManager
+
++ (int)lastInitError;
++ (void)setSharedInstanceQueue:(id)arg1;
++ (id)sharedInstance;
+
+//- (struct BTAccessoryManagerImpl { }*)_accessoryManager;
+- (void)_advertisingChanged;
+- (BOOL)_attach;
+- (void)_cleanup:(BOOL)arg1;
+- (void)_connectabilityChanged;
+- (void)_connectedStatusChanged;
+- (void)_discoveryStateChanged;
+- (void)_pairedStatusChanged;
+- (void)_postNotification:(id)arg1;
+- (void)_postNotificationWithArray:(id)arg1;
+- (void)_powerChanged;
+- (void)_removeDevice:(id)arg1;
+- (void)_restartScan;
+- (void)_scanForServices:(unsigned int)arg1 withMode:(int)arg2;
+- (void)_setScanState:(int)arg1;
+//- (BOOL)_setup:(struct BTSessionImpl { }*)arg1;
+- (void)acceptSSP:(int)arg1 forDevice:(id)arg2;
+//- (id)addDeviceIfNeeded:(struct BTDeviceImpl { }*)arg1;
+- (BOOL)audioConnected;
+- (BOOL)available;
+- (void)cancelPairing;
+- (void)connectDevice:(id)arg1;
+- (void)connectDevice:(id)arg1 withServices:(unsigned int)arg2;
+- (BOOL)connectable;
+- (BOOL)connected;
+- (id)connectedDevices;
+- (id)connectingDevices;
+- (void)dealloc;
+- (BOOL)devicePairingEnabled;
+- (BOOL)deviceScanningEnabled;
+- (BOOL)deviceScanningInProgress;
+- (void)disconnectDevice:(id)arg1;
+- (void)enableTestMode;
+- (BOOL)enabled;
+- (void)endVoiceCommand:(id)arg1;
+- (id)init;
+- (BOOL)isAnyoneAdvertising;
+- (BOOL)isAnyoneScanning;
+- (BOOL)isDiscoverable;
+- (BOOL)isServiceSupported:(unsigned int)arg1;
+- (id)localAddress;
+- (id)pairedDevices;
+- (void)postNotification:(id)arg1;
+- (void)postNotificationName:(id)arg1 object:(id)arg2;
+- (void)postNotificationName:(id)arg1 object:(id)arg2 error:(id)arg3;
+- (int)powerState;
+- (BOOL)powered;
+- (void)resetDeviceScanning;
+- (void)scanForConnectableDevices:(unsigned int)arg1;
+- (void)scanForServices:(unsigned int)arg1;
+- (void)setAudioConnected:(BOOL)arg1;
+- (void)setConnectable:(BOOL)arg1;
+- (void)setDevicePairingEnabled:(BOOL)arg1;
+- (void)setDeviceScanningEnabled:(BOOL)arg1;
+- (void)setDiscoverable:(BOOL)arg1;
+- (BOOL)setEnabled:(BOOL)arg1;
+- (void)setPincode:(id)arg1 forDevice:(id)arg2;
+- (BOOL)setPowered:(BOOL)arg1;
+- (void)showPowerPrompt;
+- (void)startVoiceCommand:(id)arg1;
+- (void)unpairDevice:(id)arg1;
+- (BOOL)wasDeviceDiscovered:(id)arg1;
+
+ // Image: /System/Library/PrivateFrameworks/GameKitServices.framework/GameKitServices
+
+- (int)localDeviceSupportsService:(unsigned int)arg1;
+
+ @end
diff --git a/Loop/Managers/Bluetooth/BluetoothManagerHandler.h b/Loop/Managers/Bluetooth/BluetoothManagerHandler.h
new file mode 100644
index 0000000000..0b2a4e7dcc
--- /dev/null
+++ b/Loop/Managers/Bluetooth/BluetoothManagerHandler.h
@@ -0,0 +1,40 @@
+/*
+ This file is part of BeeTee Project. It is subject to the license terms in the LICENSE file found in the top-level directory of this distribution and at https://github.com/michaeldorner/BeeTee/blob/master/LICENSE. No part of BeeTee Project, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the LICENSE file.
+
+ The MIT License (MIT)
+
+ Copyright (c) 2016 Michael Dorner
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+#import
+
+
+@interface BluetoothManagerHandler : NSObject
+
++ (BluetoothManagerHandler*) sharedInstance;
+
+- (bool) powered;
+- (void) setPower: (bool)powerStatus;
+- (bool) enabled;
+- (void) disable;
+- (void) enable;
+
+ @end
diff --git a/Loop/Managers/Bluetooth/BluetoothManagerHandler.m b/Loop/Managers/Bluetooth/BluetoothManagerHandler.m
new file mode 100644
index 0000000000..262442b2b7
--- /dev/null
+++ b/Loop/Managers/Bluetooth/BluetoothManagerHandler.m
@@ -0,0 +1,97 @@
+/*
+ This file is part of BeeTee Project. It is subject to the license terms in the LICENSE file found in the top-level directory of this distribution and at https://github.com/michaeldorner/BeeTee/blob/master/LICENSE. No part of BeeTee Project, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the LICENSE file.
+
+ The MIT License (MIT)
+
+ Copyright (c) 2016 Michael Dorner
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+#import "BluetoothManagerHandler.h"
+#import "BluetoothManager.h"
+
+static BluetoothManager *_bluetoothManager = nil;
+static BluetoothManagerHandler *_handler = nil;
+
+@implementation BluetoothManagerHandler
+
+
++ (BluetoothManagerHandler*) sharedInstance {
+
+ //static dispatch_once_t onceToken;
+ //dispatch_once(&onceToken, ^{
+ if (!_handler) {
+ NSBundle *b = [NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/BluetoothManager.framework"];
+ if (![b load]) {
+ NSLog(@"Error"); // maybe throw an exception
+ } else {
+ @try {
+ _bluetoothManager = [NSClassFromString(@"BluetoothManager") valueForKey:@"sharedInstance"];
+ _handler = [[BluetoothManagerHandler alloc] init];
+ }
+ @catch(NSException *e ) {
+ NSLog(@"Gosh!!! Error initializing BluetoothManager!! %@ %@ %@", [e name], [e reason], [e callStackSymbols]);
+ }
+ }
+ }
+ //});
+ return _handler;
+}
+
+
+ - (bool) powered {
+ return [_bluetoothManager powered];
+ }
+
+
+ - (void) setPower: (bool)powerStatus {
+ [_bluetoothManager setPowered:powerStatus];
+ }
+
+
+ - (void) startScan {
+ [_bluetoothManager setDeviceScanningEnabled: true];
+ [_bluetoothManager scanForServices: 0xFFFFFFFF];
+ }
+
+
+ - (void) stopScan {
+ [_bluetoothManager setDeviceScanningEnabled: false];
+ }
+
+
+ - (bool)isScanning {
+ return [_bluetoothManager deviceScanningEnabled];
+ }
+
+
+ - (bool)enabled {
+ return [_bluetoothManager enabled];
+ }
+
+ - (void)disable {
+ [_bluetoothManager setEnabled:false];
+ }
+
+ - (void)enable {
+ [_bluetoothManager setEnabled:true];
+ }
+
+ @end
diff --git a/Loop/Managers/Bluetooth/Loop2-Bridging-Header.h b/Loop/Managers/Bluetooth/Loop2-Bridging-Header.h
new file mode 100644
index 0000000000..e9efb0adfd
--- /dev/null
+++ b/Loop/Managers/Bluetooth/Loop2-Bridging-Header.h
@@ -0,0 +1,5 @@
+//
+// Use this file to import your target's public headers that you would like to expose to Swift.
+//
+
+#import "BluetoothManagerHandler.h"
diff --git a/Loop/Managers/CGM/CGMManager.swift b/Loop/Managers/CGM/CGMManager.swift
index 82424119b3..db5c8b04d2 100644
--- a/Loop/Managers/CGM/CGMManager.swift
+++ b/Loop/Managers/CGM/CGMManager.swift
@@ -7,7 +7,7 @@
import HealthKit
import LoopUI
-
+import CGMBLEKit
/// Describes the result of a CGM manager operation
///
@@ -47,6 +47,8 @@ protocol CGMManager: CustomDebugStringConvertible {
var managedDataInterval: TimeInterval? { get }
var sensorState: SensorDisplayable? { get }
+
+ var latestG5Reading: Glucose? { get }
/// The representation of the device for use in HealthKit
var device: HKDevice? { get }
@@ -59,3 +61,7 @@ protocol CGMManager: CustomDebugStringConvertible {
func fetchNewDataIfNeeded(with deviceManager: DeviceDataManager, _ completion: @escaping (CGMResult) -> Void) -> Void
}
+extension CGMManager {
+ var latestG5Reading: Glucose? { return nil }
+}
+
diff --git a/Loop/Managers/CGM/DexCGMManager.swift b/Loop/Managers/CGM/DexCGMManager.swift
index 30fd0fbaa3..da0f987861 100644
--- a/Loop/Managers/CGM/DexCGMManager.swift
+++ b/Loop/Managers/CGM/DexCGMManager.swift
@@ -36,6 +36,8 @@ class DexCGMManager: CGMManager {
return shareManager?.sensorState
}
+ var latestG5Reading: Glucose? { return nil }
+
var managedDataInterval: TimeInterval? {
return shareManager?.managedDataInterval
}
@@ -64,6 +66,8 @@ final class ShareClientManager: CGMManager {
var sensorState: SensorDisplayable? {
return latestBackfill
}
+
+ var latestG5Reading: Glucose? { return nil }
let managedDataInterval: TimeInterval? = nil
@@ -81,7 +85,7 @@ final class ShareClientManager: CGMManager {
return
}
- shareClient.fetchLast(6) { (error, glucose) in
+ shareClient.fetchLast(12) { (error, glucose) in
if let error = error {
completion(.error(error))
return
@@ -116,10 +120,7 @@ final class ShareClientManager: CGMManager {
final class G5CGMManager: DexCGMManager, TransmitterDelegate {
- func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) {
- // Not implemented yet
- }
-
+
private let transmitter: Transmitter?
let logger = DiagnosticLogger.shared!.forCategory("G5CGMManager")
@@ -150,6 +151,8 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate {
}
}
+ override var latestG5Reading: Glucose? { return latestReading }
+
override var managedDataInterval: TimeInterval? {
if let transmitter = transmitter, transmitter.passiveModeEnabled {
return .hours(3)
@@ -165,7 +168,6 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate {
latestGlucose.readDate > Date(timeIntervalSinceNow: .minutes(-4.5)) else {
return false
}
-
return true
}
@@ -175,7 +177,7 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate {
completion(.noData)
return
}
-
+ NSLog("G5CGMManager data is stale, fetch from Share: \(String(describing: latestReading))")
super.fetchNewDataIfNeeded(with: deviceManager, completion)
}
@@ -215,7 +217,6 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate {
delegate?.cgmManager(self, didUpdateWith: .noData)
return
}
-
latestReading = glucose
guard glucose.state.hasReliableGlucose else {
@@ -228,12 +229,32 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate {
delegate?.cgmManager(self, didUpdateWith: .noData)
return
}
-
+
self.delegate?.cgmManager(self, didUpdateWith: .newData([
(quantity: quantity, date: glucose.readDate, isDisplayOnly: glucose.isDisplayOnly)
]))
}
+ func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) {
+ let samples = glucose.compactMap { (glucose) -> (quantity: HKQuantity, date: Date, isDisplayOnly: Bool)? in
+ guard glucose != latestReading, glucose.state.hasReliableGlucose, let quantity = glucose.glucose else {
+ return nil
+ }
+
+ return (
+ date: glucose.readDate,
+ quantity: quantity,
+ isDisplayOnly: glucose.isDisplayOnly
+ )
+ }
+
+ guard samples.count > 0 else {
+ return
+ }
+
+ self.delegate?.cgmManager(self, didUpdateWith: .newData(samples))
+ }
+
func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) {
logger.error("Unknown sensor data: " + data.hexadecimalString)
// This can be used for protocol discovery, but isn't necessary for normal operation
diff --git a/Loop/Managers/CGM/EnliteCGMManager.swift b/Loop/Managers/CGM/EnliteCGMManager.swift
index 25a9c23358..0f9d468bf7 100644
--- a/Loop/Managers/CGM/EnliteCGMManager.swift
+++ b/Loop/Managers/CGM/EnliteCGMManager.swift
@@ -11,7 +11,6 @@ import LoopUI
import MinimedKit
import RileyLinkKit
-
final class EnliteCGMManager: CGMManager {
let providesBLEHeartbeat = false
diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift
index eaa78103e2..ad798baa07 100644
--- a/Loop/Managers/DeviceDataManager.swift
+++ b/Loop/Managers/DeviceDataManager.swift
@@ -61,7 +61,7 @@ final class DeviceDataManager {
if let status = latestPumpStatusFromMySentry {
return Double(status.batteryRemainingPercent) / 100
} else if let status = latestPumpStatus {
- return batteryChemistry.chargeRemaining(voltage: status.batteryVolts)
+ return batteryChemistry.chargeRemaining(at: status.batteryVolts)
} else {
return statusExtensionManager.context?.batteryPercentage
}
@@ -79,6 +79,7 @@ final class DeviceDataManager {
if let oldVal = oldVal, newVal - oldVal >= 0.5 {
AnalyticsManager.shared.pumpBatteryWasReplaced()
+ self.loopManager.addBatteryChange("Old: \(oldVal), New: \(newVal)")
}
}
}
@@ -162,6 +163,7 @@ final class DeviceDataManager {
self.cgmManager(self.cgmManager!, didUpdateWith: result)
}
}
+ maybeToggleBluetooth("rileyLink")
}
func connectToRileyLink(_ device: RileyLinkDevice) {
@@ -220,7 +222,17 @@ final class DeviceDataManager {
let lastTuned = deviceState.lastTuned ?? .distantPast
if lastTuned.timeIntervalSinceNow <= -tuneTolerance {
+
+ // Assume 1 device and don't tune if we had a successful comm. in the last 55
+ // minutes.
+ // TODO track the last successful communication attempt in RileyLink
+ if let reservoir = loopManager?.doseStore.lastReservoirValue,
+ reservoir.startDate.timeIntervalSinceNow > TimeInterval(minutes: -24) {
+ return
+ }
+
pumpOps.runSession(withName: "Tune pump", using: device) { (session) in
+ StatisticsManager.shared.inc("Tune pump")
do {
let scanResult = try session.tuneRadio(current: deviceState.lastValidFrequency)
self.logger.addError("Device \(device.name ?? "") auto-tuned to \(scanResult.bestFrequency) MHz", fromSource: "RileyLink")
@@ -232,12 +244,15 @@ final class DeviceDataManager {
)
}
} catch let error {
- self.logger.addError("Device \(device.name ?? "") auto-tune failed with error: \(error)", fromSource: "RileyLink")
+ self.logger.addError("troubleshootPumpComms - deprioritize device \(device.name ?? "") auto-tune failed with error: \(error)", fromSource: "RileyLink")
+ StatisticsManager.shared.inc("Deprioritize")
self.rileyLinkManager.deprioritize(device)
self.setLastError(error: error)
}
}
} else {
+ self.logger.addError("troubleshootPumpComms - deprioritize device \(device.name ?? "")", fromSource: "RileyLink")
+ StatisticsManager.shared.inc("Deprioritize")
rileyLinkManager.deprioritize(device)
}
}
@@ -253,6 +268,79 @@ final class DeviceDataManager {
}
}
+ /** Check if pump date is current and otherwise update it.
+ * TODO this should get a device name probably.
+ **/
+ private func assertPumpDate(_ date: Date) -> Bool {
+ let dateDiff = abs(date.timeIntervalSinceNow)
+ if dateDiff > TimeInterval(minutes: 1) {
+ guard let pumpOps = pumpOps else {
+ return false
+ }
+ rileyLinkManager.getDevices { (devices) in
+ guard let device = devices.firstConnected else {
+ return
+ }
+ // TODO use a session
+ pumpOps.runSession(withName: "Sync Pump Time", using: device) { (session) in
+ StatisticsManager.shared.inc("Sync Pump Time")
+ do {
+ try session.setTime { () -> DateComponents in
+ let calendar = Calendar(identifier: .gregorian)
+ return calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date())
+ }
+ self.loopManager.addInternalNote("syncPumpTime success (difference \(dateDiff)).")
+
+ } catch let error {
+
+ self.loopManager.addInternalNote("syncPumpTime error \(String(describing: error)).")
+ }
+ }
+ }
+ return false
+ }
+ return true
+ }
+
+ public func setBasalRate(_ completion: @escaping (_ error: Error?) -> Void) {
+ guard let schedule = loopManager.basalRateSchedule else {
+ NSLog("setBasalRates - no basal rate schedule")
+ completion(LoopError.configurationError("No basal rate schedule"))
+ return
+ }
+ var entries : [BasalScheduleEntry] = [];
+ var i = 0
+ for item in schedule.items {
+ let rate = item.value
+ let offset = item.startTime
+ let e = BasalScheduleEntry(index: i, timeOffset: offset, rate: rate)
+ entries.append(e)
+ i += 1
+ }
+ let newSchedule = BasalSchedule(entries: entries)
+ NSLog("setBasalRates - \(String(describing: newSchedule)).")
+ guard let ops = pumpOps else {
+ NSLog("setBasalRates - no pump ops")
+ completion(LoopError.configurationError("No pump ops"))
+ return
+ }
+ ops.runSession(withName: "setBasalRates", using: rileyLinkManager.firstConnectedDevice) { (session) in
+ StatisticsManager.shared.inc("setBasalRates")
+ guard let session = session else {
+ self.loopManager.addInternalNote("setBasalRates connection error.")
+ completion(LoopError.connectionError)
+ return
+ }
+ do {
+ try session.setBasalSchedule(newSchedule, for: .standard)
+ completion(nil)
+ } catch let error {
+ self.loopManager.addInternalNote("setBasalRates error \(String(describing: error)).")
+ completion(error)
+ }
+ }
+ }
+
/**
Handles receiving a MySentry status message, which are only posted by MM x23 pumps.
@@ -276,7 +364,11 @@ final class DeviceDataManager {
guard status != latestPumpStatusFromMySentry, let pumpDate = pumpDateComponents.date else {
return
}
-
+
+ if !assertPumpDate(pumpDate) {
+ return
+ }
+
observeBatteryDuring {
latestPumpStatusFromMySentry = status
}
@@ -344,6 +436,7 @@ final class DeviceDataManager {
- parameter timeLeft: The approximate time before the reservoir is empty
*/
private func updateReservoirVolume(_ units: Double, at date: Date, withTimeLeft timeLeft: TimeInterval?) {
+ StatisticsManager.shared.inc("updateReservoirVolume")
loopManager.addReservoirValue(units, at: date) { (result) in
/// TODO: Isolate to queue
@@ -384,6 +477,8 @@ final class DeviceDataManager {
if newValue.unitVolume > previousVolume + 1 {
AnalyticsManager.shared.reservoirWasRewound()
+ self.loopManager.addInsulinChange("Old: \(previousVolume), New: \(newValue.unitVolume)")
+ // self.loopManager.addSiteChange("Implicit with Insulin Change")
}
}
}
@@ -393,6 +488,8 @@ final class DeviceDataManager {
}
}
+ private var lastPumpHistorySuccess : Date = Date().addingTimeInterval(TimeInterval(hours:-6))
+
/// TODO: Isolate to queue
/// Polls the pump for new history events and passes them to the loop manager
///
@@ -411,11 +508,16 @@ final class DeviceDataManager {
return
}
+
ops.runSession(withName: "Fetch Pump History", using: device) { (session) in
+ StatisticsManager.shared.inc("Fetch Pump History")
do {
// TODO: This should isn't safe to access synchronously
- let startDate = self.loopManager.doseStore.pumpEventQueryAfterDate
-
+ let startDate = min(
+ self.loopManager.doseStore.pumpEventQueryAfterDate,
+ self.lastPumpHistorySuccess)
+ NSLog("fetchPumpHistory: since \(startDate)")
+
let (events, model) = try session.getHistoryEvents(since: startDate)
self.loopManager.addPumpEvents(events, from: model) { (error) in
if let error = error {
@@ -424,8 +526,37 @@ final class DeviceDataManager {
completion(error)
}
+
+ for event in events {
+ self.lastPumpHistorySuccess = max(
+ self.lastPumpHistorySuccess, event.date)
+ switch event.pumpEvent {
+ case let bg as BGReceivedPumpEvent:
+ let mgdl = bg.amount
+ let glucose = HKQuantity(unit: HKUnit.milligramsPerDeciliter(), doubleValue: Double(mgdl))
+ NSLog("Got BG event from pump, adding to glucosestore, but only if no other glucose was recently entered \(mgdl) \(glucose)")
+ self.loopManager.glucoseStore?.getGlucoseValues(start: Date().addingTimeInterval(TimeInterval(minutes: -30)), completion: { (result) in
+ switch(result) {
+ case .success(let values):
+ if values.count > 0 {
+ return
+ }
+ default:
+ break
+ }
+
+ self.loopManager.glucoseStore?.addGlucose(glucose, date: event.date, isDisplayOnly: false, device: nil) { (success, _, error) in
+ NSLog("Added BG from pump \(success), \(String(describing: error))")
+ }
+ })
+
+ default:
+ break
+ }
+ }
} catch let error {
self.troubleshootPumpComms(using: device)
+ StatisticsManager.shared.inc("fetchPumpHistory error")
self.logger.addError("Failed to fetch history: \(error)", fromSource: "RileyLink")
completion(error)
@@ -434,6 +565,13 @@ final class DeviceDataManager {
}
}
+ private var needPumpDataRead : Bool = false
+ public func triggerPumpDataRead() {
+ needPumpDataRead = true
+ NSLog("triggerPumpDataRead")
+ assertCurrentPumpData()
+ }
+
/// TODO: Isolate to queue
private func isPumpDataStale() -> Bool {
// How long should we wait before we poll for new pump data?
@@ -454,17 +592,22 @@ final class DeviceDataManager {
lastReservoirDate = max(components.date ?? .distantPast, lastReservoirDate)
}
- return lastReservoirDate.timeIntervalSinceNow <= timeIntervalSinceNow
+ return lastReservoirDate.timeIntervalSinceNow <= timeIntervalSinceNow || needPumpDataRead
}
+ private var pumpDataReadInProgress = false
+
/**
Ensures pump data is current by either waking and polling, or ensuring we're listening to sentry packets.
*/
/// TODO: Isolate to queue
- private func assertCurrentPumpData() {
+ private func assertCurrentPumpData(attempt: Int = 0) {
rileyLinkManager.assertIdleListening(forcingRestart: true)
guard isPumpDataStale() else {
+ if attempt > 0 {
+ self.pumpDataReadInProgress = false
+ }
return
}
@@ -473,16 +616,31 @@ final class DeviceDataManager {
let error = LoopError.connectionError
self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink")
self.setLastError(error: error)
+ if attempt > 0 {
+ self.pumpDataReadInProgress = false
+ }
return
}
guard let ops = self.pumpOps else {
let error = LoopError.configurationError("Pump ID")
self.setLastError(error: error)
+ if attempt > 0 {
+ self.pumpDataReadInProgress = false
+ }
return
}
+
+ if self.pumpDataReadInProgress && attempt == 0 {
+ NSLog("readAndProcessPumpData: Previous pump read still in progress, dropping this request.")
+ self.needPumpDataRead = false
+ return
+ } else {
+ self.pumpDataReadInProgress = true
+ }
ops.runSession(withName: "Get Pump Status", using: device) { (session) in
+ StatisticsManager.shared.inc("Get Pump Status")
let nsPumpStatus: NightscoutUploadKit.PumpStatus?
do {
let status = try session.getCurrentPumpStatus()
@@ -506,16 +664,32 @@ final class DeviceDataManager {
}
self.updateReservoirVolume(status.reservoir, at: date, withTimeLeft: nil)
- let battery = BatteryStatus(voltage: status.batteryVolts, status: BatteryIndicator(batteryStatus: status.batteryStatus))
+
+ let battery = BatteryStatus(voltage: status.batteryVolts.converted(to: .volts).value, status: BatteryIndicator(batteryStatus: status.batteryStatus))
nsPumpStatus = NightscoutUploadKit.PumpStatus(clock: date, pumpID: status.pumpID, iob: nil, battery: battery, suspended: status.suspended, bolusing: status.bolusing, reservoir: status.reservoir)
+
+ self.needPumpDataRead = false
} catch let error {
- self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink")
+ if attempt < 3 {
+ let nextAttempt = attempt + 1
+ // Too noisy
+ // self.loopManager.addDebugNote("readAndProcessPumpData, attempt \(nextAttempt).")
+ NSLog("readAndProcessPumpData, attempt \(nextAttempt), error \(error)")
+
+ self.assertCurrentPumpData(attempt: nextAttempt)
+ return
+ }
+ self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "assertCurrentPumpData")
self.setLastError(error: error)
self.troubleshootPumpComms(using: device)
self.nightscoutDataManager.uploadLoopStatus(loopError: error)
nsPumpStatus = nil
}
+ self.pumpDataReadInProgress = false
+
+ // Technically not true, but otherwise we create a big fast loop of unsuccessful attempts.
+ self.needPumpDataRead = false
device.getStatus { (status) in
self.queue.async {
@@ -526,21 +700,27 @@ final class DeviceDataManager {
}
}
+ private var bolusInProgress = false
+
/// TODO: Isolate to queue
/// Send a bolus command and handle the result
///
/// - parameter units: The number of units to deliver
/// - parameter completion: A clsure called after the command is complete. This closure takes a single argument:
/// - error: An error describing why the command failed
- func enactBolus(units: Double, at startDate: Date = Date(), completion: @escaping (_ error: Error?) -> Void) {
+ func enactBolus(units: Double, at startDate: Date = Date(), quiet : Bool = false, completion: @escaping (_ error: Error?) -> Void) {
+
let notify = { (error: Error?) -> Void in
if let error = error {
- NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate)
+ if !quiet {
+ NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate)
+ }
}
-
+ NSLog("enactBolus notify \(startDate) \(units) --\(String(describing: error))--")
+ self.bolusInProgress = false
completion(error)
}
-
+
guard units > 0 else {
notify(nil)
return
@@ -550,34 +730,75 @@ final class DeviceDataManager {
notify(LoopError.configurationError("Pump ID"))
return
}
+
+ guard !bolusInProgress else {
+ notify(LoopError.invalidData(details: "Bolus already in progress"))
+ bolusInProgress = true // notify alwasy set this to false, so reset to true...
+ return
+ }
+ bolusInProgress = true
// If we don't have recent pump data, or the pump was recently rewound, read new pump data before bolusing.
- let shouldReadReservoir = isReservoirDataOlderThan(timeIntervalSinceNow: .minutes(-6))
+ var shouldReadReservoir = isReservoirDataOlderThan(timeIntervalSinceNow: .minutes(-10))
+ var reservoirError : Error? = nil
+ if loopManager.doseStore.lastReservoirVolumeDrop < 0 {
+ reservoirError = LoopError.invalidData(details: "Last Reservoir drop negative: \(loopManager.doseStore.lastReservoirVolumeDrop) U.")
+ shouldReadReservoir = true
+ } else if let reservoir = loopManager.doseStore.lastReservoirValue, reservoir.startDate.timeIntervalSinceNow <=
+ -loopManager.recencyInterval {
+ reservoirError = LoopError.pumpDataTooOld(date: reservoir.startDate)
+ shouldReadReservoir = true
+ } else if loopManager.doseStore.lastReservoirValue == nil {
+ reservoirError = LoopError.missingDataError(details: "Reservoir Value missing", recovery: "Keep phone close.")
+ shouldReadReservoir = true
+ }
- ops.runSession(withName: "Bolus", using: rileyLinkManager.firstConnectedDevice) { (session) in
- guard let session = session else {
- notify(LoopError.connectionError)
- return
- }
+
+ rileyLinkManager.getDevices { (devices) in
+ guard let device = devices.firstConnected else {
+ notify(LoopError.connectionError)
+ return
+ }
+ ops.runSession(withName: "Bolus", using: device) { (session) in
+ StatisticsManager.shared.inc("Bolus")
if shouldReadReservoir {
+ if let e = reservoirError {
+ self.logger.addError(e, fromSource: "BolusReservoir")
+ } else {
+ self.logger.addError("Reservoir data too old", fromSource: "BolusReservoir")
+ }
do {
+ StatisticsManager.shared.inc("Bolus Read Reservoir")
let reservoir = try session.getRemainingInsulin()
-
+ if !self.assertPumpDate(reservoir.clock.date!) {
+ self.logger.addError("Pump clock is deviating too much, need to fix first.", fromSource: "enactBolus")
+ let error = PumpOpsError.rfCommsFailure("Pump clock is deviating too much.")
+ notify(SetBolusError.certain(error))
+ return
+ }
+ let semaphore = DispatchSemaphore(value: 0)
+ var success = false
self.loopManager.addReservoirValue(reservoir.units, at: reservoir.clock.date!) { (result) in
switch result {
case .failure(let error):
- self.logger.addError(error, fromSource: "Bolus")
+ self.logger.addError(error, fromSource: "BolusAddReservoir")
+ notify(error)
case .success:
- break
+ success = true
}
+ semaphore.signal()
+ }
+ semaphore.wait()
+ if (!success) {
+ return
}
} catch let error as PumpOpsError {
- self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink")
+ self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "enactBolus")
notify(SetBolusError.certain(error))
return
} catch let error as PumpCommandError {
- self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink")
+ self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "enactBolus")
switch error {
case .arguments(let error):
notify(SetBolusError.certain(error))
@@ -590,23 +811,73 @@ final class DeviceDataManager {
return
}
}
-
- do {
- let semaphore = DispatchSemaphore(value: 0)
- self.loopManager.addRequestedBolus(units: units, at: Date()) {
- semaphore.signal()
- }
- semaphore.wait()
-
- try session.setNormalBolus(units: units)
- self.loopManager.addConfirmedBolus(units: units, at: Date()) {
- notify(nil)
+ let semaphore = DispatchSemaphore(value: 0)
+ self.loopManager.addRequestedBolus(units: units, at: Date()) {
+ semaphore.signal()
+ }
+ semaphore.wait()
+
+ var retry = true
+ var attempt = 1
+ while retry {
+ do {
+ StatisticsManager.shared.inc("Bolus setNormalBolus")
+ try session.setNormalBolus(units: units)
+ self.loopManager.addConfirmedBolus(units: units, at: Date()) {
+ // self.triggerPumpDataRead()
+ notify(nil)
+ }
+ retry = false
+ } catch let error {
+ self.logger.addError(error, fromSource: "Bolus")
+
+ let str = "\(error)"
+
+ switch(error) {
+ case SetBolusError.certain(_):
+ // this should check it is the same bolus...
+ if str.contains("bolusInProgress") || str.contains("Bolus in progress") {
+ self.loopManager.addConfirmedBolus(units: units, at: Date()) {
+ self.loopManager.addInternalNote("retryBolus - already in progress, confirming.")
+ // self.triggerPumpDataRead()
+ notify(nil)
+ }
+ retry = false
+ }
+ case SetBolusError.uncertain(_):
+ if (str.contains("noResponse(") || str.contains("unknownResponse(")) && str.contains("powerOn") {
+ retry = true
+ } else {
+ retry = false
+ }
+ default:
+ // self.loopManager.addInternalNote("enactBolus unknown Error.")
+ retry = false
+ }
+ if retry && attempt < 5 {
+ attempt += 1
+ self.logger.addError("Bolus failed: \(error.localizedDescription), retrying attempt \(attempt)", fromSource: "enactBolus")
+ } else {
+
+ self.loopManager.addFailedBolus(units: units, at: Date(), error: error, certain: retry, attempts: attempt) {
+ if retry {
+ // If we exhausted attempts, try another connection if multiple devices are configured.
+ // Ideally this whole logic is wrapped in a retrier going through all devices.
+ self.troubleshootPumpComms(using: device)
+ } else {
+ // In case of an uncertain error, we need to read the pump data to make sure
+ // we are not double bolusing.
+ self.triggerPumpDataRead()
+ }
+ self.logger.addError("Bolus failed: \(error.localizedDescription) Retry \(retry) Attempt \(attempt)", fromSource: "enactBolus")
+ notify(error)
+ }
+ return
+ }
}
- } catch let error {
- self.logger.addError(error, fromSource: "Bolus")
- notify(error)
}
- }
+ } // newSession
+ } // getDevice
}
// MARK: - CGM
@@ -782,6 +1053,103 @@ final class DeviceDataManager {
setupCGM()
}
+
+ // MARK: - Bluetooth restart magic
+ private var btMagicDate : Date = Date()
+ func maybeToggleBluetooth(_ source: String, force: Bool = false) {
+ self.queue.async { // to avoid duplicate runs, btMagicDate.
+ var restartReason : String? = nil
+ if let reservoir = self.loopManager?.doseStore.lastReservoirValue,
+ reservoir.startDate.timeIntervalSinceNow <= TimeInterval(minutes: -30) {
+ restartReason = "pump"
+
+ } else if let glucose = self.loopManager?.glucoseStore?.latestGlucose,
+ glucose.startDate.timeIntervalSinceNow <= TimeInterval(minutes: -30) {
+ restartReason = "cgm"
+ }
+ /* Not sure if this is working.
+ if let bluetoothManagerHandler = BluetoothManagerHandler.sharedInstance() {
+ if !bluetoothManagerHandler.enabled() {
+ loopManager.addInternalNote("maybeToggleBluetooth - enable - because it was disabled")
+ bluetoothManagerHandler.enable()
+ bluetoothManagerHandler.setPower(true)
+ }
+ } */
+ guard let reason = restartReason else {
+ return
+ }
+ if self.btMagicDate.timeIntervalSinceNow > TimeInterval(minutes: -30) {
+ NSLog("maybeToggleBluetooth - \(source) - tried recently \(self.btMagicDate)")
+ return
+ }
+ self.btMagicDate = Date()
+
+ self.logger.addError("\(source) - Reason \(reason) - Restarting Bluetooth, no data for 30 minutes (could also be out of range)", fromSource: "maybeToggleBluetooth")
+
+ if reason == "pump" {
+ self.logger.addError("pump reconnect", fromSource: "maybeToggleBluetooth")
+ self.rileyLinkManager.getDevices { (devices) in
+ for device in devices {
+ if self.connectedPeripheralIDs.contains(device.peripheralIdentifier.uuidString) {
+ self.logger.addError("pump reconnect async \(device.peripheralIdentifier.uuidString)", fromSource: "maybeToggleBluetooth")
+ self.rileyLinkManager.disconnect(device)
+ self.rileyLinkManager.connect(device)
+ AnalyticsManager.shared.didChangeRileyLinkConnectionState()
+ self.logger.addError("pump reconnect done \(device.peripheralIdentifier.uuidString)", fromSource: "maybeToggleBluetooth")
+ } else {
+ self.logger.addError("pump not connected \(device.peripheralIdentifier.uuidString)", fromSource: "maybeToggleBluetooth")
+ }
+ }
+ }
+
+ }
+ else if reason == "cgm" {
+ self.logger.addError("cgm re-setup", fromSource: "maybeToggleBluetooth")
+ self.setupCGM()
+ /*
+ if let bluetoothManagerHandler = BluetoothManagerHandler.sharedInstance() {
+ self.logger.addError("disable", fromSource: "maybeToggleBluetooth")
+ bluetoothManagerHandler.disable()
+ bluetoothManagerHandler.setPower(false)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
+ self.logger.addError("ensable", fromSource: "maybeToggleBluetooth")
+ bluetoothManagerHandler.setPower(true)
+ bluetoothManagerHandler.enable()
+ })
+ } else {
+ self.logger.addError("BluetoothManagerHandler not available", fromSource: "maybeToggleBluetooth")
+ }
+ */
+ }
+ else {
+ self.logger.addError("BluetoothManagerHandler no valid reason: \(reason)", fromSource: "maybeToggleBluetooth")
+
+ }
+ }
+ }
+
+ // MARK - CGM State
+ private var lastG5SessionStart : Date? = nil
+ private var lastG5TransmitterStart : Date? = nil
+
+
+ func updateCGMState() {
+ guard let glucose = cgmManager?.latestG5Reading else {
+ return
+ }
+ let sessionStart = glucose.sessionStartDate
+ let transmitterStart = glucose.activationDate
+ let transmitterID = glucose.transmitterID
+ let transmitterState = glucose.status
+ if UserDefaults.appGroup.G5SessionStartDate != sessionStart {
+ loopManager.addSensorStart(sessionStart, "Transmitter Start \(transmitterStart), Status \(transmitterState), ID \(transmitterID)")
+ UserDefaults.appGroup.G5SessionStartDate = sessionStart
+ }
+
+ if !glucose.state.hasReliableGlucose {
+ loopManager.addInternalNote("Glucose Unreliable \(glucose.readDate), State \(glucose.state)")
+ }
+ }
}
@@ -807,8 +1175,9 @@ extension DeviceDataManager: CGMManagerDelegate {
self.setLastError(error: error)
self.assertCurrentPumpData()
}
-
+ updateCGMState()
updateTimerTickPreference()
+ maybeToggleBluetooth("cgmManager")
}
func startDateToFilterNewData(for manager: CGMManager) -> Date? {
@@ -845,16 +1214,22 @@ extension DeviceDataManager: DoseStoreDelegate {
extension DeviceDataManager: LoopDataManagerDelegate {
func loopDataManager(
- _ manager: LoopDataManager,
- didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date),
- completion: @escaping (_ result: Result) -> Void
+ _ manager: LoopDataManager,
+ didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date),
+ completion: @escaping (_ result: Result) -> Void
) {
+ internalSetTempBasal(manager, basal, completion: completion)
+ }
+
+ func internalSetTempBasal(_ manager: LoopDataManager, _ basal: (recommendation: TempBasalRecommendation, date: Date), attempt: Int = 0, completion: @escaping (_ result: Result) -> Void) {
+
guard let pumpOps = pumpOps else {
completion(.failure(LoopError.configurationError("Pump ID")))
return
}
pumpOps.runSession(withName: "Set Temp Basal", using: rileyLinkManager.firstConnectedDevice) { (session) in
+ StatisticsManager.shared.inc("Set Temp Basal")
guard let session = session else {
completion(.failure(LoopError.connectionError))
return
@@ -873,13 +1248,28 @@ extension DeviceDataManager: LoopDataManagerDelegate {
value: response.rate,
unit: .unitsPerHour
)))
-
// Continue below
} catch let error {
- completion(.failure(error))
- return
+ // notify(.failure(error))
+ let str = "\(error)"
+ // 6 seems rather high as setTempBasal already does 3 retries.
+ if attempt < 3, !str.contains("Bolus in progress") {
+ // typically sequence might be:
+ // Error: unexpectedResponse(PumpMessage(carelink, getPumpModel, 355347, 0903373534000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000), PumpMessage(carelink, powerOn, 355347, 00)),
+ // Error: rileyLinkTimeout, attempt 2
+
+ // Error: noResponse("Sent PumpMessage(carelink, powerOn, 355347, 020101000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000)"), attempt 3
+
+
+ let nextAttempt = attempt + 1
+ self.logger.addError("\(error), attempt \(nextAttempt)", fromSource: "internalSetTempBasal")
+ self.internalSetTempBasal(manager, basal, attempt: nextAttempt, completion: completion)
+ return
+ } else {
+ completion(.failure(error))
+ }
}
-
+
do {
// If we haven't fetched history in a while, our preferredInsulinDataSource is probably .reservoir, so
// let's take advantage of the pump radio being on.
@@ -894,7 +1284,7 @@ extension DeviceDataManager: LoopDataManagerDelegate {
return calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date())
}
}
-
+
self.fetchPumpHistory { (error) in
if let error = error {
self.logger.addError("Post-basal history fetch failed: \(error)", fromSource: "RileyLink")
@@ -906,6 +1296,46 @@ extension DeviceDataManager: LoopDataManagerDelegate {
}
}
}
+
+ func loopDataManager(_ manager: LoopDataManager, didRecommendBolus bolus: (recommendation: BolusRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) {
+
+ enactBolus(units: bolus.recommendation.amount, quiet: true) { (error) in
+ if let error = error {
+ completion(.failure(error))
+ } else {
+ let now = Date()
+ completion(.success(DoseEntry(
+ type: .bolus,
+ startDate: now,
+ endDate: now,
+ value: bolus.recommendation.amount,
+ unit: .units
+ )))
+ }
+ }
+ }
+
+ func loopDataManager(_ manager: LoopDataManager, uploadTreatments treatments: [NightscoutTreatment], completion: @escaping (Result<[String]>) -> Void) {
+
+ guard let uploader = remoteDataManager.nightscoutService.uploader else {
+ completion(.failure(LoopError.configurationError("Nightscout not configured")))
+ return
+ }
+
+ uploader.upload(treatments) { (result) in
+ switch result {
+ case .success(let objects):
+ completion(.success(objects))
+ case .failure(let error):
+ let logger = DiagnosticLogger.shared!.forCategory("NightscoutUploader")
+ logger.error(error)
+ NSLog("UPLOADING delegate failed \(error)")
+ completion(.failure(error))
+
+ }
+ }
+
+ }
}
@@ -940,6 +1370,7 @@ extension DeviceDataManager: CustomDebugStringConvertible {
"cgm: \(String(describing: cgm))",
"connectedPeripheralIDs: \(String(reflecting: connectedPeripheralIDs))",
"deviceStates: \(String(reflecting: deviceStates))",
+ "btMagicDate: \(String(reflecting: btMagicDate))",
"lastError: \(String(describing: lastError))",
"lastTimerTick: \(String(describing: lastTimerTick))",
"latestPumpStatus: \(String(describing: latestPumpStatus))",
diff --git a/Loop/Managers/DiagnosticLogger.swift b/Loop/Managers/DiagnosticLogger.swift
index bbc1dbbb1c..68feb7711e 100644
--- a/Loop/Managers/DiagnosticLogger.swift
+++ b/Loop/Managers/DiagnosticLogger.swift
@@ -27,6 +27,8 @@ final class DiagnosticLogger {
}
}
+ public var loopManager : LoopDataManager? = nil
+
let remoteLogLevel: OSLogType
static var shared: DiagnosticLogger?
@@ -103,6 +105,51 @@ final class CategoryLogger {
logger.mLabService.uploadTaskWithData(messageData, inCollection: category)?.resume()
}
}
+
+ private func loopLog(_ type: OSLogType, message: [String: Any]) {
+ loopLog(type, message: message.debugDescription)
+ }
+
+ private var lastMessage : String = ""
+ private var duplicateMessageCount = 0
+
+ private func loopLog(_ type: OSLogType, message: String) {
+ if let loop = self.logger.loopManager {
+ if category == "NightscoutUploader" {
+ NSLog("NightscoutUploader \(type.tagName) \(message)")
+ return
+ }
+ if category == "NightscoutService" {
+ NSLog("NightscoutService \(type.tagName) \(message)")
+ return
+ }
+ if category == "GlucoseStore" && message.range(of: "Protected health data is inaccessible") != nil {
+ return
+ }
+ if category == "RileyLink" {
+ NSLog("RileyLink \(type.tagName) \(message)")
+ return
+ }
+ if message.range(of: "NSURLErrorDomain") != nil {
+ return
+ }
+ if message.range(of: "updatePredicted getLoopState") != nil {
+ NSLog("\(type.tagName) \(message)")
+ return
+ }
+ if (message == lastMessage) {
+ duplicateMessageCount += 1
+ return
+ } else {
+ if (duplicateMessageCount > 0) {
+ loop.addDebugNote("Logger: x\(duplicateMessageCount): \(message)")
+ }
+ duplicateMessageCount = 0
+ }
+ lastMessage = message
+ loop.addDebugNote("Logger: \(category) \(type.tagName) \(message)")
+ }
+ }
func debug(_ message: [String: Any]) {
systemLog.debug("%{public}@", String(describing: message))
@@ -127,11 +174,13 @@ final class CategoryLogger {
func error(_ message: [String: Any]) {
systemLog.error("%{public}@", String(reflecting: message))
remoteLog(.error, message: message)
+ loopLog(.error, message: message)
}
func error(_ message: String) {
systemLog.error("%{public}@", message)
remoteLog(.error, message: message)
+ loopLog(.error, message: message)
}
func error(_ error: Error) {
diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift
index 10c0872e8f..126be04bbc 100644
--- a/Loop/Managers/DoseMath.swift
+++ b/Loop/Managers/DoseMath.swift
@@ -44,10 +44,14 @@ extension InsulinCorrection {
/// - Returns: A temp basal recommendation
fileprivate func asTempBasal(
scheduledBasalRate: Double,
+ minBasalRate: Double,
maxBasalRate: Double,
+ insulinOnBoard: Double,
+ maxInsulinOnBoard: Double,
duration: TimeInterval,
minimumProgrammableIncrementPerUnit: Double
) -> TempBasalRecommendation {
+ let units = Swift.min(self.units, Swift.max(0, maxInsulinOnBoard - insulinOnBoard))
var rate = units / (duration / TimeInterval(hours: 1)) // units/hour
switch self {
case .aboveRange, .inRange, .entirelyBelowRange:
@@ -56,8 +60,9 @@ extension InsulinCorrection {
break
}
- rate = Swift.min(maxBasalRate, Swift.max(0, rate))
rate = round(rate * minimumProgrammableIncrementPerUnit) / minimumProgrammableIncrementPerUnit
+ // Limit to min/max rate within the limits
+ rate = Swift.min(maxBasalRate, Swift.max(minBasalRate, rate))
return TempBasalRecommendation(
unitsPerHour: rate,
@@ -90,16 +95,35 @@ extension InsulinCorrection {
fileprivate func asBolus(
pendingInsulin: Double,
maxBolus: Double,
+ insulinOnBoard: Double,
+ maxInsulinOnBoard: Double,
minimumProgrammableIncrementPerUnit: Double
) -> BolusRecommendation {
- var units = self.units - pendingInsulin
- units = Swift.min(maxBolus, Swift.max(0, units))
- units = round(units * minimumProgrammableIncrementPerUnit) / minimumProgrammableIncrementPerUnit
-
+ let netUnits = self.units - pendingInsulin
+ var units = Swift.min(maxBolus, Swift.max(0, netUnits))
+ units = Swift.min(units, Swift.max(0, maxInsulinOnBoard - insulinOnBoard))
+ units = floor(units * minimumProgrammableIncrementPerUnit) / minimumProgrammableIncrementPerUnit
+ var target : HKQuantity?
+ var minPrediction : GlucoseValue?
+
+ switch(self) {
+ case .entirelyBelowRange(
+ correcting: let min,
+ minTarget: let minTarget,
+ units: _
+ ):
+ minPrediction = min
+ target = minTarget
+ default:
+ break
+ }
return BolusRecommendation(
amount: units,
pendingInsulin: pendingInsulin,
- notice: bolusRecommendationNotice
+ notice: bolusRecommendationNotice,
+ netAmount: netUnits,
+ target: target,
+ minPrediction: minPrediction
)
}
}
@@ -245,12 +269,12 @@ extension Collection where Iterator.Element == GlucoseValue {
guard validDateRange.contains(prediction.startDate) else {
continue
}
-
+
// If any predicted value is below the suspend threshold, return immediately
- guard prediction.quantity >= suspendThreshold else {
- return .suspend(min: prediction)
- }
-
+ //guard prediction.quantity >= suspendThreshold else {
+ // return .suspend(min: prediction)
+ //}
+
// Update range statistics
if minGlucose == nil || prediction.quantity < minGlucose!.quantity {
minGlucose = prediction
@@ -315,7 +339,12 @@ extension Collection where Iterator.Element == GlucoseValue {
) else {
return nil
}
-
+
+ // If belowRange is predicted, but we are below the suspendThreshold -> suspend.
+ if min.quantity < suspendThreshold {
+ return .suspend(min: min)
+ }
+
return .entirelyBelowRange(
correcting: min,
minTarget: HKQuantity(unit: unit, doubleValue: minGlucoseTargets.minValue),
@@ -324,6 +353,19 @@ extension Collection where Iterator.Element == GlucoseValue {
} else if eventualGlucoseValue > eventualGlucoseTargets.maxValue,
let minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose
{
+ // If we are going above range, but cross through some values below suspendThreshold -
+ // give some insulin in any case. The reasoning is that this is typical for correcting
+ // a low and if we don't give anything, we risk overshooting afterwards. Having a
+ // prediction above means usually we are looking at a series with carbs in it.
+ if min.quantity < suspendThreshold {
+ return .aboveRange(
+ min: min,
+ correcting: correctingGlucose,
+ minTarget: HKQuantity(unit: unit, doubleValue: eventualGlucoseTargets.minValue),
+ units: minCorrectionUnits * 0.6
+ )
+ }
+
return .aboveRange(
min: min,
correcting: correctingGlucose,
@@ -331,6 +373,11 @@ extension Collection where Iterator.Element == GlucoseValue {
units: minCorrectionUnits
)
} else {
+ // If inRange is predicted, but we are below the suspendThreshold -> suspend.
+ if min.quantity < suspendThreshold {
+ return .suspend(min: min)
+ }
+
return .inRange
}
}
@@ -358,9 +405,13 @@ extension Collection where Iterator.Element == GlucoseValue {
suspendThreshold: HKQuantity?,
sensitivity: InsulinSensitivitySchedule,
model: InsulinModel,
+ minBasalRates: BasalRateSchedule,
basalRates: BasalRateSchedule,
maxBasalRate: Double,
+ insulinOnBoard: Double,
+ maxInsulinOnBoard: Double,
lastTempBasal: DoseEntry?,
+ lowerOnly: Bool = false, // only lower the basal, never raise
duration: TimeInterval = .minutes(30),
minimumProgrammableIncrementPerUnit: Double = 40,
continuationInterval: TimeInterval = .minutes(11)
@@ -374,6 +425,7 @@ extension Collection where Iterator.Element == GlucoseValue {
)
let scheduledBasalRate = basalRates.value(at: date)
+ let minBasalRate = minBasalRates.value(at: date)
var maxBasalRate = maxBasalRate
// TODO: Allow `highBasalThreshold` to be a configurable setting
@@ -382,10 +434,17 @@ extension Collection where Iterator.Element == GlucoseValue {
{
maxBasalRate = scheduledBasalRate
}
+
+ if lowerOnly {
+ maxBasalRate = scheduledBasalRate
+ }
let temp = correction?.asTempBasal(
scheduledBasalRate: scheduledBasalRate,
+ minBasalRate: minBasalRate,
maxBasalRate: maxBasalRate,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
duration: duration,
minimumProgrammableIncrementPerUnit: minimumProgrammableIncrementPerUnit
)
@@ -418,7 +477,9 @@ extension Collection where Iterator.Element == GlucoseValue {
model: InsulinModel,
pendingInsulin: Double,
maxBolus: Double,
- minimumProgrammableIncrementPerUnit: Double = 40
+ insulinOnBoard: Double,
+ maxInsulinOnBoard: Double,
+ minimumProgrammableIncrementPerUnit: Double = 10
) -> BolusRecommendation {
guard let correction = self.insulinCorrection(
to: correctionRange,
@@ -433,6 +494,8 @@ extension Collection where Iterator.Element == GlucoseValue {
var bolus = correction.asBolus(
pendingInsulin: pendingInsulin,
maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard,
+ maxInsulinOnBoard: maxInsulinOnBoard,
minimumProgrammableIncrementPerUnit: minimumProgrammableIncrementPerUnit
)
diff --git a/Loop/Managers/FoodManager.swift b/Loop/Managers/FoodManager.swift
new file mode 100644
index 0000000000..be86dd97f9
--- /dev/null
+++ b/Loop/Managers/FoodManager.swift
@@ -0,0 +1,441 @@
+//
+// FoodManager.swift
+// Loop
+//
+// Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+import CarbKit
+
+enum AbsorptionSpeed {
+ case ultraFast // like glucose tabs
+ case fast // like juice
+ case normal
+ case slow // like pizza
+
+ func encode() -> String {
+ switch(self) {
+ case .ultraFast: return "ultrafast"
+ case .fast: return "fast"
+ case .normal: return "normal"
+ case .slow: return "slow"
+ }
+ }
+}
+
+extension AbsorptionSpeed {
+ init?(raw: String) {
+ switch(raw) {
+ case "ultrafast":
+ self = .ultraFast
+ case "fast":
+ self = .fast
+ case "slow":
+ self = .slow
+ case "normal":
+ self = .normal
+ default:
+ return nil
+ }
+ }
+
+ var minutes : Double {
+ let multiplier = LoopSettings().absorptionTimeMultiplier
+ switch(self) {
+ case .ultraFast:
+ return round(multiplier * 90)
+ case .fast:
+ return round(multiplier * 120)
+ case .normal:
+ return round(multiplier * 180)
+ case .slow:
+ return round(multiplier * 300)
+ }
+ }
+ var seconds : Double {
+ return minutes * 60
+ }
+}
+
+struct FoodItem {
+ let carbRatio : Double // 14g / 100g = 0.14
+ let portionSize : Double // 240g
+
+
+ let absorption : AbsorptionSpeed
+ let title : String
+
+ var quantity : HKQuantity {
+ return HKQuantity(unit: HKUnit.gram(), doubleValue: Double(carbPortion))
+ }
+
+ var carbPortion : Double {
+ return portionSize * carbRatio
+ }
+}
+
+extension FoodItem {
+ init?(rawValues : [String: Any]) {
+ carbRatio = rawValues["carbRatio"] as? Double ?? 0
+ portionSize = rawValues["portionSize"] as? Double ?? 0
+ title = rawValues["title"] as? String ?? ""
+ absorption = rawValues["absorption"] as? AbsorptionSpeed ?? .normal
+ }
+
+ public func encode() -> [String: Any] {
+ return [
+ "carbRatio": carbRatio,
+ "portionSize": portionSize,
+ "title": title,
+ "absorption": absorption.encode()
+ ]
+
+ }
+}
+
+struct FoodPick : CustomStringConvertible {
+ let item : FoodItem
+ let ratio : Double
+ let date : Date
+ let imageIdentifier : String?
+
+ // Internally generated
+ var quantity : HKQuantity {
+ return HKQuantity(unit: HKUnit.gram(), doubleValue: carbs)
+ }
+
+ var carbEntry : CarbEntry {
+ let representation : [Any] = [self.encode()]
+ var foodType = item.title
+ do {
+ let data = try JSONSerialization.data(withJSONObject: representation, options: [])
+ if let encodedData = String(data: data, encoding: .utf8) {
+ foodType = encodedData
+ }
+ } catch {
+ // TODO(Erik)
+ }
+ return NewCarbEntry(quantity: quantity, startDate: date, foodType: foodType,
+ absorptionTime: item.absorption.seconds)
+ }
+
+ var carbs : Double {
+ return item.carbPortion * ratio
+ }
+
+ var displayPortion : String {
+ let intPortion = Int(round(item.portionSize * ratio))
+ return "\(intPortion)"
+ }
+
+ var displayCarbs : String {
+ let intCarbs = Int(round(carbs))
+ return "\(intCarbs)"
+ }
+
+ var description : String {
+ let cr = Int(item.carbRatio * 100)
+ return "\(displayPortion)g \(item.title): \(displayCarbs)g KH (\(cr)%)"
+ }
+
+ init(item : FoodItem, ratio: Double, date : Date) {
+ self.init(item: item, ratio: ratio, date: date, imageIdentifier: nil)
+ }
+
+ init(item : FoodItem, ratio: Double, date : Date, imageIdentifier: String?) {
+ self.item = item
+ self.ratio = ratio
+ self.date = date
+ self.imageIdentifier = imageIdentifier
+ }
+
+}
+
+extension FoodPick {
+ init(rawValues : [String: Any]) {
+ let it = FoodItem(rawValues: rawValues["item"] as! [String : Any])
+ let rat = rawValues["ratio"] as? Double ?? 1.0
+ let dat = Date(timeIntervalSince1970: rawValues["date"] as? Double ?? 0)
+ let image = rawValues["image"] as? String
+ self.init(item: it!, ratio: rat, date: dat, imageIdentifier: image)
+ }
+
+ func encode() -> [String: Any] {
+ var ret : [String:Any] = [
+ "item": item.encode(),
+ "ratio": ratio,
+ "date": date.timeIntervalSince1970
+ ]
+ if let image = imageIdentifier {
+ ret["image"] = image
+ }
+ return ret
+ }
+
+
+
+}
+
+struct FoodPicks {
+ var picks : [FoodPick] = []
+
+ var last : FoodPick? {
+ return picks.last
+ }
+
+ var carbs : Double {
+ var sum : Double = 0
+ for pick in picks {
+ sum += pick.carbs
+ }
+ return sum
+ }
+
+ mutating func append(_ pick : FoodPick) {
+ picks.append(pick)
+ }
+
+ mutating func removeLast() -> FoodPick? {
+ if picks.count > 0 {
+ return picks.removeLast()
+ }
+ return nil
+ }
+
+ func toJSON() -> String? {
+ var representation : [Any] = []
+ for pick in picks {
+ representation.append(pick.encode())
+ }
+
+ do {
+ let data = try JSONSerialization.data(withJSONObject: representation, options: [])
+
+ return String(data: data, encoding: .utf8)
+ } catch let error {
+ NSLog("FoodPicks JSON representation error \(error)")
+ return nil
+ }
+ }
+
+ init() {
+ self.picks = []
+ }
+
+ init(fromJSON: String) {
+ do {
+ let json = try JSONSerialization.jsonObject(with: fromJSON.data(using: .utf8)!, options: JSONSerialization.ReadingOptions())
+ for rawValue in json as! [Any] {
+ picks.append(FoodPick(rawValues: rawValue as! [String : Any]))
+ }
+ } catch let error {
+ NSLog("FoodPicks fromJSON error \(error)")
+ }
+
+ }
+}
+
+struct FoodMetadata {
+ enum FoodType {
+ case continuous
+ case single
+ case multiple
+ case drink
+ }
+ let type : FoodType
+ let subtitle : String?
+ let image : String?
+ let initial : Double
+
+ init(_ raw: [String: Any]) {
+ switch raw["type"] as? String ?? "single" {
+ case "continuous": type = .continuous
+ case "single": type = .single
+ case "multiple": type = .multiple
+ case "drink": type = .drink
+ default: type = .single
+ }
+ subtitle = raw["subtitle"] as? String
+ image = raw["image"] as? String
+ initial = raw["initial"] as? Double ?? 1 // in case of single or multiple servings
+ }
+}
+
+final class FoodManager {
+
+ public var items : [FoodItem] = []
+ public var categories : [String: [FoodItem]] = ["Popular": []]
+ public var sections : [String] = []
+ private var itemByTitle : [String: FoodItem] = [:]
+ private var meta : [String: FoodMetadata] = [:]
+ func metaData(_ item: FoodItem) -> FoodMetadata {
+ return meta[item.title] ?? FoodMetadata([:])
+ }
+
+ public var stats : [String:[String:Int]] = [:]
+
+ func record(_ pick: FoodPick) {
+ let calendar = Calendar.current
+ let hour = calendar.component(.hour, from: pick.date)
+ let weekday = calendar.component(.weekday, from: pick.date)
+ let key = "\(weekday)-\(hour)"
+ let name = pick.item.title.lowercased()
+ if var hourly = stats[key] {
+ if let byname = hourly[name] {
+ stats[key]![name] = byname + 1
+ } else {
+ stats[key]![name] = 1
+ }
+ } else {
+ stats[key] = [name: 1]
+ }
+ NSLog("FoodManager record stats \(stats)")
+ UserDefaults.standard.foodStats = stats
+ }
+
+ let popKey = "Popular"
+
+ func updatePopular() {
+ let calendar = Calendar.current
+ let date = Date()
+ let originalHour = calendar.component(.hour, from: date)
+ let originalWeekday = calendar.component(.weekday, from: date)
+
+ var list : [(Int, FoodItem)] = []
+ var seen : [String] = []
+ let varyHour = [0, -1, 1, -2, 2]
+ let varyWeekday = [0, -1, 1, -2, 2, -3, 3]
+ for w in varyWeekday {
+ for h in varyHour {
+ let hour = (originalHour + h) % 24
+ let weekday = (originalWeekday + w) % 7
+
+ let key = "\(weekday)-\(hour)"
+ for entry in stats[key] ?? [:] {
+ if let item = itemByTitle[entry.key], !seen.contains(entry.key), entry.value > 1 {
+ list.append((entry.value, item))
+ seen.append(entry.key)
+ }
+ }
+ }
+ if list.count > 8 { break }
+ }
+ list.sort { $0.0 > $1.0 }
+ var newList : [FoodItem] = []
+ for (_, item) in list {
+ newList.append(item)
+ if newList.count > 4 { break }
+ }
+ NSLog("FoodManager updatePopular \(newList)")
+ categories[popKey] = newList
+ }
+
+ private func getDirectoryPath(_ fileName: String) -> String {
+ let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
+ let documentsDirectory = paths[0] as NSString
+ return documentsDirectory.appendingPathComponent(fileName)
+ }
+
+ func getCustomImage(_ fileName : String) -> UIImage? {
+ let fileManager = FileManager.default
+ let imagePath = getDirectoryPath(fileName)
+ if fileManager.fileExists(atPath: imagePath) {
+ return UIImage(contentsOfFile: imagePath)
+ }
+ return nil
+ }
+
+ func saveCustomImage(_ image: UIImage) -> String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss"
+
+ let fileName = dateFormatter.string(from: Date()) + ".jpg"
+
+ let fileManager = FileManager.default
+ let pathName = getDirectoryPath(fileName)
+ let imageData = UIImageJPEGRepresentation(image, 85)
+ fileManager.createFile(atPath: pathName as String, contents: imageData, attributes: nil)
+
+ UserDefaults.standard.foodManagerNeedUpload.append(fileName)
+ NSLog("FoodManager Upload Backlog \(UserDefaults.standard.foodManagerNeedUpload)")
+ return fileName
+ }
+
+ func image(item: FoodItem) -> UIImage? {
+ let meta = metaData(item)
+ if let image = meta.image {
+ return UIImage(named: "FoodCatalog/\(image).jpg")
+ }
+ return UIImage(named: "FoodCatalog/\(item.title).jpg")
+ }
+
+ func image(pick: FoodPick) -> UIImage? {
+ if let imageIdentifier = pick.imageIdentifier {
+ return getCustomImage(imageIdentifier)
+ }
+ return image(item: pick.item)
+ }
+
+ init() {
+ do {
+
+ guard let url = Bundle.main.url(forResource: "FoodCatalog/catalog", withExtension: "json") else {
+ NSLog("FoodCatalog Cannot find catalog file")
+ return
+ }
+ //let jsonData = try NSData(contentsOfFile: path, options: .mappedIfSafe)
+ let jsonData = try Data(contentsOf: url)
+
+ guard let json = try JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions()) as? [String:Any] else {
+ NSLog("FoodCatalog Cannot read json file at url \(url)")
+ return
+ }
+
+ for raw in json {
+ guard let value = raw.value as? [String: Any] else {
+ NSLog("FoodManager ignoring malformed entry \(raw)")
+ continue
+ }
+ // picks.append(FoodPick(rawValues: rawValue as! [String : Any]))
+ if let carbspercent = value["ratio"] as? Double,
+ let portionSize = value["portion"] as? Double {
+ let title = value["title"] as? String ?? raw.key
+ let absorptionString = value["absorption"] as? String ?? "normal"
+
+ let absorption = AbsorptionSpeed(raw: absorptionString) ?? .normal
+
+ let item = FoodItem(carbRatio: carbspercent / 100, portionSize: portionSize, absorption: absorption, title: title)
+ items.append(item)
+ if let s = value["categories"] as? [String] {
+ for section in s {
+ if categories[section] != nil {
+ categories[section]!.append(item)
+ } else {
+ categories[section] = [item]
+ }
+ }
+ }
+ meta[item.title] = FoodMetadata(value)
+ itemByTitle[item.title.lowercased()] = item
+ } else {
+ NSLog("FoodManager ignoring malformed entry \(raw)")
+ }
+ }
+ NSLog("FoFoodManager Categories \(categories)")
+
+ } catch let error {
+ NSLog("FoodManager FoodCatalog Read Error \(error)")
+ }
+ sections = categories.keys.sorted()
+ if let pop = sections.index(of: popKey) {
+ sections.remove(at: pop)
+ }
+ sections.insert(popKey, at: 0)
+ stats = UserDefaults.standard.foodStats
+ NSLog("FoodManager stats loaded: \(stats)")
+ }
+
+
+}
diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift
index 61a076ace0..c30b8acc34 100644
--- a/Loop/Managers/LoopDataManager.swift
+++ b/Loop/Managers/LoopDataManager.swift
@@ -12,6 +12,7 @@ import GlucoseKit
import HealthKit
import InsulinKit
import LoopKit
+import NightscoutUploadKit
final class LoopDataManager {
@@ -37,11 +38,19 @@ final class LoopDataManager {
unowned let delegate: LoopDataManagerDelegate
private let logger: CategoryLogger
+
+ var minimumBasalRateSchedule : BasalRateSchedule? {
+ didSet {
+ UserDefaults.appGroup.minimumBasalRateSchedule = minimumBasalRateSchedule
+ notify(forChange: .preferences)
+ }
+ }
init(
delegate: LoopDataManagerDelegate,
lastLoopCompleted: Date?,
lastTempBasal: DoseEntry?,
+ minimumBasalRateSchedule: BasalRateSchedule? = UserDefaults.appGroup.minimumBasalRateSchedule,
basalRateSchedule: BasalRateSchedule? = UserDefaults.appGroup.basalRateSchedule,
carbRatioSchedule: CarbRatioSchedule? = UserDefaults.appGroup.carbRatioSchedule,
insulinModelSettings: InsulinModelSettings? = UserDefaults.appGroup.insulinModelSettings,
@@ -55,20 +64,25 @@ final class LoopDataManager {
self.lastLoopCompleted = lastLoopCompleted
self.lastTempBasal = lastTempBasal
self.settings = settings
-
+ self.minimumBasalRateSchedule = minimumBasalRateSchedule
+ self.pumpDetachedMode = UserDefaults.appGroup.pumpDetachedMode
+
let healthStore = HKHealthStore()
carbStore = CarbStore(
healthStore: healthStore,
defaultAbsorptionTimes: (
- fast: TimeInterval(hours: 2),
- medium: TimeInterval(hours: 3),
- slow: TimeInterval(hours: 4)
+ fast: TimeInterval(minutes: AbsorptionSpeed.fast.minutes),
+ medium: TimeInterval(minutes: AbsorptionSpeed.normal.minutes),
+ slow: TimeInterval(minutes: AbsorptionSpeed.slow.minutes)
),
carbRatioSchedule: carbRatioSchedule,
insulinSensitivitySchedule: insulinSensitivitySchedule
)
-
+ // disable overrun as it creates dangerous late lows if the
+ // carb absorption is finished early (e.g. less input or sports).
+ carbStore.absorptionTimeOverrun = settings.absorptionTimeOverrun
+
doseStore = DoseStore(
healthStore: healthStore,
insulinModel: insulinModelSettings?.model,
@@ -84,6 +98,7 @@ final class LoopDataManager {
object: nil,
queue: nil
) { (note) -> Void in
+ NSLog("carbEntriesDidUpdate")
self.dataAccessQueue.async {
self.carbEffect = nil
self.carbsOnBoard = nil
@@ -192,13 +207,14 @@ final class LoopDataManager {
}
/// The amount of time since a given date that data should be considered valid
- var recencyInterval = TimeInterval(minutes: 15)
+ public var recencyInterval = TimeInterval(minutes: 15)
/// Sets a new time zone for a the schedule-based settings
///
/// - Parameter timeZone: The time zone
func setScheduleTimeZone(_ timeZone: TimeZone) {
self.basalRateSchedule?.timeZone = timeZone
+ self.minimumBasalRateSchedule?.timeZone = timeZone
self.carbRatioSchedule?.timeZone = timeZone
self.insulinSensitivitySchedule?.timeZone = timeZone
settings.glucoseTargetRangeSchedule?.timeZone = timeZone
@@ -277,7 +293,9 @@ final class LoopDataManager {
/// - completion: A closure called once upon completion
/// - result: The bolus recommendation
func addCarbEntryAndRecommendBolus(_ carbEntry: CarbEntry, replacing replacingEntry: CarbEntry? = nil, completion: @escaping (_ result: Result) -> Void) {
- let addCompletion: (Bool, CarbEntry?, CarbStore.CarbStoreError?) -> Void = { (success, _, error) in
+ NSLog("addCarbEntryAndRecommendBolus: \(carbEntry) \(String(describing: replacingEntry))")
+
+ let addCompletion: (Bool, CarbEntry?, CarbStore.CarbStoreError?) -> Void = { (success, entry, error) in
self.dataAccessQueue.async {
if success {
// Remove the active pre-meal target override
@@ -285,26 +303,38 @@ final class LoopDataManager {
self.carbEffect = nil
self.carbsOnBoard = nil
-
defer {
+ NSLog("addCarbEntryAndRecommendBolus notify carbs: \(String(describing: entry))")
self.notify(forChange: .carbs)
}
do {
- try self.update()
-
- completion(.success(self.recommendedBolus?.recommendation))
+ try self.update("addCarbEntryAndRecommendBolus")
+ if let bolus = self.recommendedBolus {
+ NSLog("addCarbEntryAndRecommendBolus bolus recommendation: \(bolus))")
+ completion(.success(bolus.recommendation))
+ } else {
+ // TODO(Erik) Surface the real error here
+ throw LoopError.missingDataError(details: "Cannot recommend Bolus", recovery: "Check your data")
+ }
} catch let error {
+ NSLog("addCarbEntryAndRecommendBolus bolus recommendation error: \(error))")
completion(.failure(error))
}
} else if let error = error {
+ self.logger.error("addCarbEntryAndRecommendBolus error: \(error)): \(String(describing: entry))")
+
completion(.failure(error))
} else {
+ self.logger.error("addCarbEntryAndRecommendBolus no success and no error: \(String(describing: entry))")
+
completion(.success(nil))
}
}
}
+ lastCarbChange = Date()
+
if let replacingEntry = replacingEntry {
carbStore.replaceCarbEntry(replacingEntry, withEntry: carbEntry, resultHandler: addCompletion)
} else {
@@ -319,7 +349,11 @@ final class LoopDataManager {
/// - date: The date the bolus was requested
func addRequestedBolus(units: Double, at date: Date, completion: (() -> Void)?) {
dataAccessQueue.async {
- self.lastRequestedBolus = (units: units, date: date)
+ NSLog("Bolus Requested: \(units) \(date)")
+ self.recommendedBolus = nil
+ self.lastPendingBolus = nil
+ self.lastFailedBolus = nil
+ self.lastRequestedBolus = (units: units, date: date, reservoir: self.doseStore.lastReservoirValue)
self.notify(forChange: .bolus)
completion?()
@@ -332,17 +366,45 @@ final class LoopDataManager {
/// - units: The bolus amount, in units
/// - date: The date the bolus was enacted
func addConfirmedBolus(units: Double, at date: Date, completion: (() -> Void)?) {
- self.doseStore.addPendingPumpEvent(.enactedBolus(units: units, at: date)) {
+ let event = NewPumpEvent.enactedBolus(units: units, at: date)
+ NSLog("Bolus Confirmed: \(units) \(date)")
+ self.doseStore.addPendingPumpEvent(event) {
self.dataAccessQueue.async {
+ let requestDate = self.lastRequestedBolus?.date ?? date
+ self.lastPendingBolus = (units: units, date: requestDate, reservoir: self.doseStore.lastReservoirValue, event: event)
+ self.logger.info("new pending Bolus \(units) U, StartDate: \(String(describing: event.dose?.startDate)), EndDate \(String(describing: event.dose?.endDate))")
self.lastRequestedBolus = nil
+ self.lastFailedBolus = nil
+ self.lastAutomaticBolus = date // keep this as a date, irrespective of automatic or not
+ self.recommendedBolus = nil
self.insulinEffect = nil
- self.notify(forChange: .bolus)
+ // self.carbUndoPossible = requestDate
+ self.notify(forChange: .bolus)
+ do {
+ try self.update("addConfirmedBolus")
+ } catch let error {
+ self.logger.error("Update after confirmed bolus failed \(error)")
+ }
completion?()
}
}
}
+ func addFailedBolus(units: Double, at date: Date, error: Error, certain: Bool, attempts: Int, completion: (() -> Void)?) {
+ dataAccessQueue.async {
+ NSLog("addFailedBolus: \(units) U @ \(date), \(error), Certain: \(certain), #\(attempts)")
+ self.lastFailedBolus = (units: units, date: date, error: error, certain: certain, attempts: attempts)
+ self.lastRequestedBolus = nil
+ self.lastPendingBolus = nil
+ if (!certain) {
+ self.recommendedBolus = nil
+ }
+ self.notify(forChange: .bolus)
+ completion?()
+ }
+ }
+
/// Adds and stores new pump events
///
/// - Parameters:
@@ -406,32 +468,49 @@ final class LoopDataManager {
self.setRecommendedTempBasal(completion)
}
}
-
+
/// Runs the "loop"
///
/// Executes an analysis of the current data, and recommends an adjustment to the current
/// temporary basal rate.
func loop() {
+ StatisticsManager.shared.inc("loop")
self.dataAccessQueue.async {
NotificationCenter.default.post(name: .LoopRunning, object: self)
self.lastLoopError = nil
do {
- try self.update()
+ try self.update("loop")
+ do {
+ try self.maybeSendFutureLowNotification()
+ } catch let error {
+ self.logger.error("maybeSendFutureLowNotificationError: \(error)")
+ }
+
if self.settings.dosingEnabled {
self.setRecommendedTempBasal { (error) -> Void in
self.lastLoopError = error
- if let error = error {
- self.logger.error(error)
- } else {
- self.lastLoopCompleted = Date()
+ if error == nil {
+ if self.settings.bolusEnabled {
+ // Have to do a bolus first.
+ self.setAutomatedBolus { (error) -> Void in
+ if let error = error {
+ self.logger.error("setAutomatedBolus \(error)")
+ } else {
+ self.lastLoopCompleted = Date()
+ }
+ }
+ } else {
+ // No automatic Bolus, we are done.
+ self.lastLoopCompleted = Date()
+ }
}
self.notify(forChange: .tempBasal)
}
-
+
// Delay the notification until we know the result of the temp basal
return
} else {
@@ -440,7 +519,6 @@ final class LoopDataManager {
} catch let error {
self.lastLoopError = error
}
-
self.notify(forChange: .tempBasal)
}
}
@@ -459,20 +537,45 @@ final class LoopDataManager {
/// - LoopError.glucoseTooOld
/// - LoopError.missingDataError
/// - LoopError.pumpDataTooOld
- fileprivate func update() throws {
+ fileprivate func update(_ reason: String) throws {
+ NSLog("update - \(reason)")
+ StatisticsManager.shared.inc("update")
dispatchPrecondition(condition: .onQueue(dataAccessQueue))
let updateGroup = DispatchGroup()
// Fetch glucose effects as far back as we want to make retroactive analysis
var latestGlucoseDate: Date?
+ var momentumInterval: TimeInterval?
updateGroup.enter()
glucoseStore.getCachedGlucoseValues(start: Date(timeIntervalSinceNow: -recencyInterval)) { (values) in
latestGlucoseDate = values.last?.startDate
+
+ // Find the first value which is within the 15 minute momentumInterval (defined in LoopKit/GlucoseStore)
+ // This is to prevent the momentum do take extreme values if e.g. a BG Meter and CGM value are at the
+ // same time, but vastly different.
+ if let last = latestGlucoseDate {
+ var first = last
+ for value in values {
+ if value.startDate.timeIntervalSinceNow >= TimeInterval(minutes: -15) {
+ first = min(first, value.startDate)
+ }
+ }
+ momentumInterval = last.timeIntervalSince(first)
+ NSLog("momentumInterval \(first) \(last) \(String(describing: momentumInterval))")
+ }
+
updateGroup.leave()
}
+
_ = updateGroup.wait(timeout: .distantFuture)
guard let lastGlucoseDate = latestGlucoseDate else {
+ if let recommendation = recommendBolusCarbOnly() {
+ recommendedBolus = (recommendation: recommendation, date: Date())
+ } else {
+ recommendedBolus = nil
+ }
+ _ = updateGroup.wait(timeout: .distantFuture)
throw LoopError.missingDataError(details: "Glucose data not available", recovery: "Check your CGM data source")
}
@@ -497,16 +600,21 @@ final class LoopDataManager {
}
if glucoseMomentumEffect == nil {
- updateGroup.enter()
- glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in
- if let error = error, effects.count == 0 {
- self.logger.error(error)
- self.glucoseMomentumEffect = nil
- } else {
- self.glucoseMomentumEffect = effects
- }
+ if let momentumInterval = momentumInterval, momentumInterval >= TimeInterval(minutes: 4) {
+ updateGroup.enter()
+ glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in
+ if let error = error, effects.count == 0 {
+ self.logger.error("getRecentMomentumEffect \(error)")
+ self.glucoseMomentumEffect = nil
+ } else {
+ self.glucoseMomentumEffect = effects
+ }
- updateGroup.leave()
+ updateGroup.leave()
+ }
+ } else {
+ let error = LoopError.missingDataError(details: "Not enough history for momentum calculation, interval only \(String(describing: momentumInterval))", recovery: "Wait")
+ NSLog("\(error)")
}
}
@@ -515,7 +623,7 @@ final class LoopDataManager {
doseStore.getGlucoseEffects(start: retrospectiveStart) { (result) -> Void in
switch result {
case .failure(let error):
- self.logger.error(error)
+ self.logger.error("getGlucoseEffects \(error)")
self.insulinEffect = nil
case .success(let effects):
self.insulinEffect = effects
@@ -524,6 +632,26 @@ final class LoopDataManager {
updateGroup.leave()
}
}
+
+ if insulinOnBoard == nil {
+ updateGroup.enter()
+ let now = Date()
+ doseStore.getInsulinOnBoardValues(start: retrospectiveStart, end: now) { (result) in
+ switch result {
+ case .success(let value):
+ if let recentValue = value.closestPriorToDate(now) {
+ self.insulinOnBoard = recentValue
+ } else {
+ self.insulinOnBoard = InsulinValue(startDate: now, value: 0.0)
+ }
+ case .failure(let error):
+ NSLog("getInsulinOnBoardValues - error: \(error)")
+ self.logger.error("getInsulinOnBoardValues \(error)")
+ self.insulinOnBoard = nil
+ }
+ updateGroup.leave()
+ }
+ }
_ = updateGroup.wait(timeout: .distantFuture)
@@ -544,7 +672,7 @@ final class LoopDataManager {
) { (result) -> Void in
switch result {
case .failure(let error):
- self.logger.error(error)
+ self.logger.error("getGlucoseEffects \(error)")
self.carbEffect = nil
case .success(let effects):
self.carbEffect = effects
@@ -568,6 +696,7 @@ final class LoopDataManager {
}
}
+
_ = updateGroup.wait(timeout: .distantFuture)
if retrospectivePredictedGlucose == nil {
@@ -582,7 +711,7 @@ final class LoopDataManager {
do {
try updatePredictedGlucoseAndRecommendedBasalAndBolus()
} catch let error {
- logger.error(error)
+ logger.error("updatePredicted \(reason) - \(error)")
throw error
}
@@ -662,10 +791,16 @@ final class LoopDataManager {
if inputs.contains(.momentum), let momentumEffect = self.glucoseMomentumEffect {
momentum = momentumEffect
}
-
+
if inputs.contains(.retrospection) {
effects.append(self.retrospectiveGlucoseEffect)
}
+
+ // DISABLE while in trial
+ //updateHighGlucoseBoost()
+ //if let effect = highGlucoseEffect {
+ // effects.append(effect)
+ //}
var prediction = LoopMath.predictGlucose(glucose, momentum: momentum, effects: effects)
@@ -694,8 +829,16 @@ final class LoopDataManager {
private var insulinEffect: [GlucoseEffect]? {
didSet {
predictedGlucose = nil
+ insulinOnBoard = nil
}
}
+
+ fileprivate var insulinOnBoard: InsulinValue? {
+ didSet {
+ predictedGlucose = nil
+ }
+ }
+
private var glucoseMomentumEffect: [GlucoseEffect]? {
didSet {
predictedGlucose = nil
@@ -734,7 +877,11 @@ final class LoopDataManager {
fileprivate var carbsOnBoard: CarbValue?
fileprivate var lastTempBasal: DoseEntry?
- fileprivate var lastRequestedBolus: (units: Double, date: Date)?
+
+ fileprivate var lastRequestedBolus: (units: Double, date: Date, reservoir: ReservoirValue?)?
+ fileprivate var lastPendingBolus: (units: Double, date: Date, reservoir: ReservoirValue?, event: NewPumpEvent)?
+ fileprivate var lastFailedBolus: (units: Double, date: Date, error: Error, certain: Bool, attempts: Int)?
+
fileprivate var lastLoopCompleted: Date? {
didSet {
NotificationManager.scheduleLoopNotRunningNotifications()
@@ -744,14 +891,84 @@ final class LoopDataManager {
}
fileprivate var lastLoopError: Error? {
didSet {
- if lastLoopError != nil {
- AnalyticsManager.shared.loopDidError()
+ if let error = lastLoopError {
+ AnalyticsManager.shared.loopDidError(error)
}
}
}
+ /**
+ Retrospective correction math, including proportional and integral action
+ See https://github.com/LoopKit/Loop/issues/695 for discussion
+ Credit dm61
+ */
+ struct retrospectiveCorrection {
+
+ let discrepancyGain: Double
+ let persistentDiscrepancyGain: Double
+ let correctionTimeConstant: Double
+ let integralGain: Double
+ let integralForget: Double
+ let proportionalGain: Double
+ let carbEffectLimit: Double
+
+ static var effectDuration: Double = 50
+ static var previousDiscrepancy: Double = 0
+ static var integralDiscrepancy: Double = 0
+
+ init() {
+ discrepancyGain = 1.0 // high-frequency RC gain, equivalent to Loop 1.5 gain = 1
+ persistentDiscrepancyGain = 5.0 // low-frequency RC gain for persistent errors, must be >= discrepancyGain
+ correctionTimeConstant = 90.0 // correction filter time constant in minutes
+ // TODO Erik changed this to 15, now back to 30
+ // 20180507 back to 15, 30 was too aggressive
+ carbEffectLimit = 15.0 // reset integral RC if carbEffect over past 30 min is greater than carbEffectLimit expressed in mg/dL
+ let sampleTime: Double = 5.0 // sample time = 5 min
+ integralForget = exp( -sampleTime / correctionTimeConstant ) // must be between 0 and 1
+ integralGain = ((1 - integralForget) / integralForget) *
+ (persistentDiscrepancyGain - discrepancyGain)
+ proportionalGain = discrepancyGain - integralGain
+ }
+ func updateRetrospectiveCorrection(discrepancy: Double,
+ positiveLimit: Double,
+ negativeLimit: Double,
+ carbEffect: Double) -> Double {
+ if (retrospectiveCorrection.previousDiscrepancy * discrepancy < 0 ||
+ (discrepancy > 0 && carbEffect > carbEffectLimit)){
+ // reset integral action when discrepancy reverses polarity or
+ // if discrepancy is positive and carb effect is greater than carbEffectLimit
+ retrospectiveCorrection.effectDuration = 60.0
+ retrospectiveCorrection.previousDiscrepancy = 0.0
+ retrospectiveCorrection.integralDiscrepancy = integralGain * discrepancy
+ } else {
+ // update integral action via low-pass filter y[n] = forget * y[n-1] + gain * u[n]
+ retrospectiveCorrection.integralDiscrepancy =
+ integralForget * retrospectiveCorrection.integralDiscrepancy +
+ integralGain * discrepancy
+ // impose safety limits on integral retrospective correction
+ retrospectiveCorrection.integralDiscrepancy = min(max(retrospectiveCorrection.integralDiscrepancy, negativeLimit), positiveLimit)
+ retrospectiveCorrection.previousDiscrepancy = discrepancy
+ // extend duration of retrospective correction effect by 10 min, up to a maxium of 180 min
+ retrospectiveCorrection.effectDuration =
+ min(retrospectiveCorrection.effectDuration + 10, 180)
+ }
+ let overallDiscrepancy = proportionalGain * discrepancy + retrospectiveCorrection.integralDiscrepancy
+ return(overallDiscrepancy)
+ }
+ func updateEffectDuration() -> Double {
+ return(retrospectiveCorrection.effectDuration)
+ }
+ func resetRetrospectiveCorrection() {
+ retrospectiveCorrection.effectDuration = 50.0
+ retrospectiveCorrection.previousDiscrepancy = 0.0
+ retrospectiveCorrection.integralDiscrepancy = 0.0
+ return
+ }
+ }
+
/**
Runs the glucose retrospective analysis using the latest effect data.
+ Updated to include integral retrospective correction.
*This method should only be called from the `dataAccessQueue`*
*/
@@ -765,8 +982,16 @@ final class LoopDataManager {
self.retrospectivePredictedGlucose = nil
throw LoopError.missingDataError(details: "Cannot retrospect glucose due to missing input data", recovery: nil)
}
+
+ // integral retrospective correction variables
+ var dynamicEffectDuration: TimeInterval = effectDuration
+ let RC = retrospectiveCorrection()
guard let change = retrospectiveGlucoseChange else {
+ // reset integral action variables in case of calibration event
+ RC.resetRetrospectiveCorrection()
+ dynamicEffectDuration = effectDuration
+ NSLog("myLoop --- suspected calibration event, no retrospective correction")
self.retrospectivePredictedGlucose = nil
return // Expected case for calibrations
}
@@ -781,17 +1006,100 @@ final class LoopDataManager {
self.retrospectivePredictedGlucose = retrospectivePrediction
- guard let lastGlucose = retrospectivePrediction.last else { return }
+ guard let lastGlucose = retrospectivePrediction.last else {
+ RC.resetRetrospectiveCorrection()
+ NSLog("myLoop --- glucose data missing, reset retrospective correction")
+ return }
let glucoseUnit = HKUnit.milligramsPerDeciliter()
let velocityUnit = glucoseUnit.unitDivided(by: HKUnit.second())
- let discrepancy = change.end.quantity.doubleValue(for: glucoseUnit) - lastGlucose.quantity.doubleValue(for: glucoseUnit) // mg/dL
- let velocity = HKQuantity(unit: velocityUnit, doubleValue: discrepancy / change.end.endDate.timeIntervalSince(change.0.endDate))
+
+ // user settings
+ guard
+ let glucoseTargetRange = settings.glucoseTargetRangeSchedule,
+ let insulinSensitivity = insulinSensitivitySchedule,
+ let basalRates = basalRateSchedule,
+ let suspendThreshold = settings.suspendThreshold?.quantity,
+ let currentBG = glucoseStore.latestGlucose?.quantity.doubleValue(for: glucoseUnit)
+ else {
+ RC.resetRetrospectiveCorrection()
+ NSLog("myLoop --- could not get settings, reset retrospective correction")
+ return
+ }
+ let date = Date()
+ let currentSensitivity = insulinSensitivity.quantity(at: date).doubleValue(for: glucoseUnit)
+ let currentBasalRate = basalRates.value(at: date)
+ let currentMinTarget = glucoseTargetRange.minQuantity(at: date).doubleValue(for: glucoseUnit)
+ let currentSuspendThreshold = suspendThreshold.doubleValue(for: glucoseUnit)
+
+ // safety limit for + integral action: ISF * (2 hours) * (basal rate)
+ let integralActionPositiveLimit = currentSensitivity * 2 * currentBasalRate
+ // safety limit for - integral action: suspend threshold - target
+ let integralActionNegativeLimit = min(-15,-abs(currentMinTarget - currentSuspendThreshold))
+
+ // safety limit for current discrepancy
+ let discrepancyLimit = integralActionPositiveLimit
+ let currentDiscrepancyUnlimited = change.end.quantity.doubleValue(for: glucoseUnit) - lastGlucose.quantity.doubleValue(for: glucoseUnit) // mg/dL
+ let currentDiscrepancy = min(max(currentDiscrepancyUnlimited, -discrepancyLimit), discrepancyLimit)
+
+ // retrospective carb effect
+ let retrospectiveCarbEffect = LoopMath.predictGlucose(change.start, effects:
+ carbEffect.filterDateRange(startDate, endDate))
+ guard let lastCarbOnlyGlucose = retrospectiveCarbEffect.last else {
+ RC.resetRetrospectiveCorrection()
+ NSLog("myLoop --- could not get carb effect, reset retrospective correction")
+ return
+ }
+ let currentCarbEffect = -change.start.quantity.doubleValue(for: glucoseUnit) + lastCarbOnlyGlucose.quantity.doubleValue(for: glucoseUnit)
+
+ // update overall retrospective correction
+ let overallRC = RC.updateRetrospectiveCorrection(
+ discrepancy: currentDiscrepancy,
+ positiveLimit: integralActionPositiveLimit,
+ negativeLimit: integralActionNegativeLimit,
+ carbEffect: currentCarbEffect
+ )
+
+ let effectMinutes = RC.updateEffectDuration()
+ dynamicEffectDuration = TimeInterval(minutes: effectMinutes)
+
+ // retrospective correction including integral action
+ let scaledDiscrepancy = overallRC * 60.0 / effectMinutes // scaled to account for extended effect duration
+
+ // Velocity calculation had change.end.endDate.timeIntervalSince(change.0.endDate) in the denominator,
+ // which could lead to too high RC gain when retrospection interval is shorter than 30min
+ // Changed to safe fixed default retrospection interval of 30*60 = 1800 seconds
+ let velocity = HKQuantity(unit: velocityUnit, doubleValue: scaledDiscrepancy / 1800.0)
let type = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodGlucose)!
let glucose = HKQuantitySample(type: type, quantity: change.end.quantity, start: change.end.startDate, end: change.end.endDate)
-
- self.retrospectiveGlucoseEffect = LoopMath.decayEffect(from: glucose, atRate: velocity, for: effectDuration)
+ self.retrospectiveGlucoseEffect = LoopMath.decayEffect(from: glucose, atRate: velocity, for: dynamicEffectDuration)
+
+ // retrospective insulin effect (just for monitoring RC operation)
+ let retrospectiveInsulinEffect = LoopMath.predictGlucose(change.start, effects:
+ insulinEffect.filterDateRange(startDate, endDate))
+ guard let lastInsulinOnlyGlucose = retrospectiveInsulinEffect.last else { return }
+ let currentInsulinEffect = -change.start.quantity.doubleValue(for: glucoseUnit) + lastInsulinOnlyGlucose.quantity.doubleValue(for: glucoseUnit)
+
+ // retrospective delta BG (just for monitoring RC operation)
+ let currentDeltaBG = change.end.quantity.doubleValue(for: glucoseUnit) -
+ change.start.quantity.doubleValue(for: glucoseUnit)// mg/dL
+
+ // monitoring of retrospective correction in debugger or Console ("message: myLoop")
+ NSLog("myLoop ******************************************")
+ NSLog("myLoop ---retrospective correction ([mg/dL] bg unit)---")
+ NSLog("myLoop Current BG: %f", currentBG)
+ NSLog("myLoop 30-min retrospective delta BG: %f", currentDeltaBG)
+ NSLog("myLoop Retrospective insulin effect: %f", currentInsulinEffect)
+ NSLog("myLoop Retrospectve carb effect: %f", currentCarbEffect)
+ NSLog("myLoop Current discrepancy: %f", currentDiscrepancy)
+ NSLog("myLoop Overall retrospective correction: %f", overallRC)
+ NSLog("myLoop Correction effect duration [min]: %f", effectMinutes)
+ if abs(lastIRCLog.timeIntervalSinceNow) >= TimeInterval(minutes: 30) {
+ addInternalNote("IRC: \(currentDeltaBG), \(currentInsulinEffect), \(currentCarbEffect), \(currentDiscrepancy), \(overallRC), \(effectMinutes)")
+ lastIRCLog = Date()
+ }
}
+ private var lastIRCLog = Date.distantPast
/// Measure the effects counteracting insulin observed in the CGM glucose.
///
@@ -831,6 +1139,7 @@ final class LoopDataManager {
let glucoseUnit = HKUnit.milligramsPerDeciliter()
let velocityUnit = glucoseUnit.unitDivided(by: HKUnit.second())
let discrepancy = change.end.quantity.doubleValue(for: glucoseUnit) - lastGlucose.quantity.doubleValue(for: glucoseUnit) // mg/dL
+
let averageVelocity = HKQuantity(unit: velocityUnit, doubleValue: discrepancy / change.end.endDate.timeIntervalSince(change.start.endDate))
let effect = GlucoseEffectVelocity(startDate: startDate, endDate: change.end.startDate, quantity: averageVelocity)
@@ -848,7 +1157,7 @@ final class LoopDataManager {
/// - LoopError.pumpDataTooOld
private func updatePredictedGlucoseAndRecommendedBasalAndBolus() throws {
dispatchPrecondition(condition: .onQueue(dataAccessQueue))
-
+ // NSLog("updatePredictedGlucoseAndRecommendedBasal")
guard let glucose = glucoseStore.latestGlucose else {
self.predictedGlucose = nil
throw LoopError.missingDataError(details: "Glucose", recovery: "Check your CGM data source")
@@ -871,7 +1180,7 @@ final class LoopDataManager {
throw LoopError.pumpDataTooOld(date: pumpStatusDate)
}
- guard glucoseMomentumEffect != nil, carbEffect != nil, insulinEffect != nil else {
+ guard /*glucoseMomentumEffect != nil, */carbEffect != nil, insulinEffect != nil else {
self.predictedGlucose = nil
throw LoopError.missingDataError(details: "Glucose effects", recovery: nil)
}
@@ -881,8 +1190,10 @@ final class LoopDataManager {
guard let
maxBasal = settings.maximumBasalRatePerHour,
+ let maximumInsulinOnBoard = settings.maximumInsulinOnBoard,
let glucoseTargetRange = settings.glucoseTargetRangeSchedule,
let insulinSensitivity = insulinSensitivitySchedule,
+ let minBasalRates = minimumBasalRateSchedule,
let basalRates = basalRateSchedule,
let maxBolus = settings.maximumBolus,
let model = insulinModelSettings?.model
@@ -890,8 +1201,70 @@ final class LoopDataManager {
throw LoopError.configurationError("Check settings")
}
+ guard let insulinOnBoard = insulinOnBoard
+ else {
+ throw LoopError.missingDataError(details: "Insulin on Board not available (updatePredictedGlucoseAndRecommendedBasal)", recovery: "Pump data up to date?")
+ }
+
+// guard cgmCalibrated else {
+// throw LoopError.missingDataError(details: "CGM", recovery: "CGM Recently calibrated")
+// }
+
+ let tempBasal = predictedGlucose.recommendedTempBasal(
+ to: glucoseTargetRange,
+ suspendThreshold: settings.suspendThreshold?.quantity,
+ sensitivity: insulinSensitivity,
+ model: model,
+ minBasalRates: minBasalRates,
+ basalRates: basalRates,
+ maxBasalRate: maxBasal,
+ insulinOnBoard: insulinOnBoard.value,
+ maxInsulinOnBoard: maximumInsulinOnBoard,
+ lastTempBasal: lastTempBasal,
+ lowerOnly: settings.bolusEnabled,
+ minimumProgrammableIncrementPerUnit: settings.insulinIncrementPerUnit
+ )
+
+ // Don't recommend changes if a bolus was just set
+ if let temp = tempBasal, lastRequestedBolus == nil/*, (temp.duration == 0 || temp.duration >= TimeInterval(minutes: 5))*/ {
+ recommendedTempBasal = (recommendation: temp, date: Date())
+ } else {
+ NSLog("updatePredictedGlucoseAndRecommendedBasal - Bolus or !tempBasal")
+ }
+
+ do {
+ let pendingInsulin = try self.getPendingInsulin()
+
+ let recommendation = predictedGlucose.recommendedBolus(
+ to: glucoseTargetRange,
+ suspendThreshold: settings.suspendThreshold?.quantity,
+ sensitivity: insulinSensitivity,
+ model: model,
+ pendingInsulin: pendingInsulin,
+ maxBolus: maxBolus,
+ insulinOnBoard: insulinOnBoard.value,
+ maxInsulinOnBoard: maximumInsulinOnBoard,
+ minimumProgrammableIncrementPerUnit: settings.insulinIncrementPerUnit
+ )
+ recommendedBolus = (recommendation: recommendation, date: startDate)
+ //recommendedBolus = (recommendation: try recommendBolus(), date: Date())
+ } catch let error {
+ // TODO(Erik): Surface error
+ _ = error
+ NSLog("updatePredictedGlucoseAndRecommendedBasal - Bolus error: \(error)")
+ recommendedBolus = nil
+ }
+
+ if let remaining = pumpDetachedRemaining() {
+ NSLog("updatePredictedGlucoseAndRecommendedBasal - Pump Detached!")
+ recommendedTempBasal = (recommendation: TempBasalRecommendation(unitsPerHour: 0.025, duration: remaining), date: Date())
+ recommendedBolus = nil
+ }
+
guard lastRequestedBolus == nil
- else {
+ else {
+ NSLog("updatePredictedGlucoseAndRecommendedBasal - Bolus ongoing")
+
// Don't recommend changes if a bolus was just requested.
// Sending additional pump commands is not going to be
// successful in any case.
@@ -899,34 +1272,7 @@ final class LoopDataManager {
recommendedTempBasal = nil
return
}
-
- let tempBasal = predictedGlucose.recommendedTempBasal(
- to: glucoseTargetRange,
- suspendThreshold: settings.suspendThreshold?.quantity,
- sensitivity: insulinSensitivity,
- model: model,
- basalRates: basalRates,
- maxBasalRate: maxBasal,
- lastTempBasal: lastTempBasal
- )
-
- if let temp = tempBasal {
- recommendedTempBasal = (recommendation: temp, date: startDate)
- } else {
- recommendedTempBasal = nil
- }
- let pendingInsulin = try self.getPendingInsulin()
-
- let recommendation = predictedGlucose.recommendedBolus(
- to: glucoseTargetRange,
- suspendThreshold: settings.suspendThreshold?.quantity,
- sensitivity: insulinSensitivity,
- model: model,
- pendingInsulin: pendingInsulin,
- maxBolus: maxBolus
- )
- recommendedBolus = (recommendation: recommendation, date: startDate)
}
/// *This method should only be called from the `dataAccessQueue`*
@@ -957,6 +1303,502 @@ final class LoopDataManager {
}
}
}
+
+ /// *This method should only be called from the `dataAccessQueue`*
+ private var lastAutomaticBolus : Date? = nil
+ private var lastCarbChange : Date? = nil
+
+ private func roundInsulinUnits(_ units: Double) -> Double {
+ return floor(units * settings.insulinIncrementPerUnit)/settings.insulinIncrementPerUnit
+ }
+
+ private func setAutomatedBolus(_ completion: @escaping (_ error: Error?) -> Void) {
+ dispatchPrecondition(condition: .onQueue(dataAccessQueue))
+
+
+ guard let recommendedBolus = self.recommendedBolus else {
+ completion(nil)
+ NSLog("setAutomatedBolus - recommendation not available")
+ return
+ }
+
+ let safeAmount = roundInsulinUnits(recommendedBolus.recommendation.amount * settings.automatedBolusRatio)
+ if safeAmount < settings.automatedBolusThreshold {
+ completion(nil)
+ NSLog("setAutomatedBolus - recommendation below threshold")
+ return
+ }
+
+ guard abs(recommendedBolus.date.timeIntervalSinceNow) < TimeInterval(minutes: 5) else {
+ completion(LoopError.recommendationExpired(date: recommendedBolus.date))
+ addInternalNote("setAutomatedBolus - recommendation too old")
+ return
+ }
+
+ if let lastAutomaticBolus = self.lastAutomaticBolus, abs(lastAutomaticBolus.timeIntervalSinceNow) < settings.automaticBolusInterval {
+ NSLog("setAutomatedBolus - last automatic bolus too close")
+ StatisticsManager.shared.inc("AutomatedBolus TooClose")
+ completion(nil)
+ return
+ }
+
+ if let carbChange = lastCarbChange {
+ guard abs(carbChange.timeIntervalSinceNow) > TimeInterval(minutes: 2) else {
+ addInternalNote("setAutomatedBolus - last carbchange too close")
+ completion(nil)
+ return
+ }
+ }
+ // TODO lastPendingBolus is never cleared, thus we need to check for the date here.
+ if lastRequestedBolus != nil {
+ addInternalNote("setAutomatedBolus - lastRequestedBolus still in progress \(String(describing: lastRequestedBolus))")
+ completion(nil)
+ return
+ }
+ if let lastPendingBolus = lastPendingBolus, let dose = lastPendingBolus.event.dose, dose.endDate.timeIntervalSinceNow > TimeInterval(minutes: 0) {
+ addInternalNote("setAutomatedBolus - lastPendingBolus still in progress \(String(describing: lastPendingBolus))")
+ completion(nil)
+ return
+ }
+ // copy bolus with "safe" ratio
+ let automatedBolus = (recommendation: BolusRecommendation(amount: safeAmount , pendingInsulin: recommendedBolus.recommendation.pendingInsulin, notice: recommendedBolus.recommendation.notice ), date: recommendedBolus.date)
+ addInternalNote("AutomatedBolus: \(automatedBolus), l")
+ self.recommendedBolus = nil
+ lastAutomaticBolus = Date()
+
+ delegate.loopDataManager(self, didRecommendBolus: automatedBolus) { (result) in
+ self.dataAccessQueue.async {
+ switch result {
+ case .success(let bolus):
+ // TODO(Erik) Do we need to do something with the bolus here?
+ // self.lastTempBasal = basal
+ self.addInternalNote("AutomatedBolus - success: \(bolus.units) U")
+ self.recommendedBolus = nil
+
+ completion(nil)
+ case .failure(let error):
+ completion(error)
+ }
+ }
+ }
+ }
+
+ // TREATMENT STATE
+
+ fileprivate func getTreatmentInformation() -> TreatmentInformation? {
+ let now = Date()
+ var treatment : TreatmentInformation?
+
+ var allowed : Bool = false
+ var message = ""
+ if let reservoir = doseStore.lastReservoirValue {
+ if reservoir.startDate.timeIntervalSinceNow >= TimeInterval(minutes: -15) {
+ allowed = true
+ } else {
+ message = "Pump data too old"
+ }
+ } else {
+ message = "Pump data not available"
+ }
+ // TODO(Erik): sent, failed, maybefailed are missing
+ if let lastRequestedBolus = lastRequestedBolus {
+ // Bolus in Progress
+ treatment = TreatmentInformation(state: .sent,
+ units: lastRequestedBolus.units,
+ carbs: 0.0,
+ date: now,
+ sent: lastRequestedBolus.date,
+ allowed: false,
+ message: "",
+ reservoir: nil,
+ attempts: nil)
+
+ } else if let lastPendingBolus = lastPendingBolus, let dose = lastPendingBolus.event.dose, dose.endDate.timeIntervalSinceNow > TimeInterval(0) {
+ if let start = lastPendingBolus.reservoir, let current = doseStore.lastReservoirValue {
+ let drop = roundInsulinUnits(start.unitVolume - current.unitVolume)
+ let units = roundInsulinUnits(lastPendingBolus.units)
+ message = "\(drop)/\(units) U"
+ }
+ treatment = TreatmentInformation(state: .pending, // TODO(Erik): Pending and wait for real conf.
+ units: lastPendingBolus.units,
+ carbs: 0.0,
+ date: lastPendingBolus.date,
+ sent: nil,
+ allowed: false,
+ message: message,
+ reservoir: nil,
+ attempts: nil)
+
+
+ } else if let lastPendingBolus = lastPendingBolus, lastPendingBolus.date.timeIntervalSinceNow > TimeInterval(minutes: -15) {
+ treatment = TreatmentInformation(state: .success, // TODO(Erik): Pending and wait for real conf.
+ units: lastPendingBolus.units,
+ carbs: 0.0,
+ date: lastPendingBolus.date,
+ sent: nil,
+ allowed: allowed,
+ message: message,
+ reservoir: nil,
+ attempts: nil)
+
+ } else if let lastFailedBolus = lastFailedBolus, lastFailedBolus.date.timeIntervalSinceNow > TimeInterval(minutes: -15) {
+ if lastFailedBolus.certain {
+ treatment = TreatmentInformation(state: .failed,
+ units: lastFailedBolus.units,
+ carbs: 0.0,
+ date: lastFailedBolus.date,
+ sent: nil,
+ allowed: allowed,
+ message: lastFailedBolus.error.localizedDescription,
+ reservoir: nil,
+ attempts: lastFailedBolus.attempts)
+ } else {
+ treatment = TreatmentInformation(state: .maybefailed,
+ units: lastFailedBolus.units,
+ carbs: 0.0,
+ date: lastFailedBolus.date,
+ sent: nil,
+ allowed: allowed,
+ message: lastFailedBolus.error.localizedDescription,
+ reservoir: nil,
+ attempts: lastFailedBolus.attempts)
+ }
+ } else if let recommended = recommendedBolus, recommended.recommendation.amount >= settings.minimumRecommendedBolus, allowed {
+ treatment = TreatmentInformation(state: .recommended,
+ units: recommended.recommendation.amount,
+ carbs: 0.0,
+ date: recommended.date,
+ sent: nil,
+ allowed: allowed,
+ message: message,
+ reservoir: nil,
+ attempts: nil)
+ } else if let low = lastLowNotification, low.date.timeIntervalSinceNow > TimeInterval(minutes: -15) {
+ treatment = TreatmentInformation(state: .recommended,
+ units: 0.0,
+ carbs: low.carbs,
+ date: low.date,
+ sent: nil,
+ allowed: allowed,
+ message: message,
+ reservoir: nil,
+ attempts: nil)
+
+// } else if let recommended = recommendedBolus, recommended.recommendation.netAmount < 0,
+// let carbRatio = carbRatioSchedule?.value(at: recommended.date) {
+//
+// let carbs = round(abs(recommended.recommendation.netAmount) * carbRatio / 5) * 5
+// treatment = TreatmentInformation(state: .recommended,
+// units: 0.0,
+// carbs: carbs,
+// date: recommended.date,
+// sent: nil,
+// allowed: allowed,
+// message: message,
+// reservoir: nil)
+
+ } else if !allowed {
+ treatment = TreatmentInformation(state: .prohibited,
+ units: 0.0,
+ carbs: 0.0,
+ date: now,
+ sent: nil,
+ allowed: allowed,
+ message: message,
+ reservoir: nil,
+ attempts: nil)
+ } else {
+ treatment = TreatmentInformation(state: .none,
+ units: 0,
+ carbs: 0.0,
+ date: now,
+ sent: Date(),
+ allowed: allowed,
+ message: message,
+ reservoir: nil,
+ attempts: nil)
+ }
+ return treatment
+ }
+
+ // PUMP DETACH MODE
+ fileprivate var pumpDetachedMode : Date? {
+ didSet {
+ UserDefaults.appGroup.pumpDetachedMode = pumpDetachedMode
+
+ notify(forChange: .preferences)
+ }
+ }
+
+ public func enablePumpDetachedMode() {
+ dataAccessQueue.async {
+ self.pumpDetachedMode = Date().addingTimeInterval(TimeInterval(minutes: 120)) // far future
+ // self.deviceDataManager.nightscoutDataManager.uploadNote(note: "Enabled Pump Detached Mode")
+ }
+ }
+
+ public func disablePumpDetachedMode() {
+ dataAccessQueue.async {
+ self.pumpDetachedMode = nil
+ // self.deviceDataManager.nightscoutDataManager.uploadNote(note: "Disabled Pump Detached Mode")
+ }
+ }
+
+ private func pumpDetachedRemaining() -> TimeInterval? {
+ dispatchPrecondition(condition: .onQueue(dataAccessQueue))
+ if let pumpDetachedMode = pumpDetachedMode {
+ let remaining = pumpDetachedMode.timeIntervalSinceNow
+ if remaining > TimeInterval(0) {
+ return remaining
+ } else {
+ self.pumpDetachedMode = nil
+ }
+ }
+ return nil
+ }
+
+ // FOOD PICKS
+ private var manualGlucoseEntered = false // TODO switch to false
+
+ public func removeCarbEntry(carbEntry: CarbEntry, _ completion: @escaping (_ error: Error?) -> Void) {
+
+ self.logger.error("removeCarbEntry - original \(carbEntry)")
+ carbStore.deleteCarbEntry(carbEntry) { (success, error) in
+ self.dataAccessQueue.async {
+ self.carbEffect = nil
+ self.carbsOnBoard = nil
+ defer {
+ self.notify(forChange: .carbs)
+ }
+ }
+ DispatchQueue.main.async {
+ // TODO: CarbStore doesn't automatically post this for deletes
+ NotificationCenter.default.post(name: .CarbEntriesDidUpdate, object: self)
+ }
+ if success {
+ completion(nil)
+ } else if let err = error {
+ NSLog("removeCarbEntry deleteCarbEntry error: \(err)")
+ completion(error)
+ }
+ }
+ }
+
+ // CARB ONLY BOLUS SUGGESTION
+
+ func recommendBolusCarbOnly() -> BolusRecommendation? {
+ // lastBolus is a bit bad here as automatedBolus can overwrite this for unsuccessful
+ // bolus' as well.
+ guard let carbRatioRange = carbRatioSchedule else {
+ return nil
+ }
+ let halfAnHourAgo = Date().addingTimeInterval(TimeInterval(minutes:-30))
+ let lastBolus = lastAutomaticBolus ?? halfAnHourAgo
+ let since = max(lastBolus, halfAnHourAgo)
+ var carbs = 0.0
+ let updateGroup = DispatchGroup()
+ updateGroup.enter()
+ // since last bolus, but not more than 30 minutes
+ carbStore.getCachedCarbEntries(start: since) { (values) in
+
+ for value in values {
+ carbs = carbs + value.quantity.doubleValue(for: HKUnit.gram())
+
+ }
+
+ updateGroup.leave()
+ }
+ updateGroup.wait()
+ let carbRatio = carbRatioRange.quantity(at: Date()).doubleValue(for: HKUnit.gram())
+ let recommendation = floor(carbs / carbRatio * 10) / 10
+ do {
+ let pendingInsulin = try self.getPendingInsulin()
+
+ let safeRecommendation = Swift.min(settings.maximumBolus ?? 0, recommendation)
+ if recommendation > 0 {
+ self.addInternalNote("recommendBolusCarbOnly - Ratio \(carbRatio) - Carbs \(carbs) - Since \(since) - recommendation \(recommendation) U, limited to \(safeRecommendation) U.")
+ }
+ return BolusRecommendation(amount: safeRecommendation, pendingInsulin: pendingInsulin,
+ notice: .carbOnly(carbs: carbs, originalAmount: (safeRecommendation < recommendation) ? recommendation : nil))
+ } catch {
+ return nil
+ }
+
+ }
+
+ // VALID GLUCOSE HELPERS
+ public func getValidGlucose() -> GlucoseValue? {
+ if let
+ glucose = self.glucoseStore?.latestGlucose {
+ let startDate = Date()
+
+ if startDate.timeIntervalSince(glucose.startDate) <= recencyInterval {
+ return glucose
+ }
+ }
+ return nil
+ }
+
+ private func getValidPredictedGlucose() -> [GlucoseValue]? {
+ // If Loop is running
+ if let predicted_glucose = self.predictedGlucose,
+ let predictedInterval = predicted_glucose.first?.startDate.timeIntervalSinceNow,
+ abs(predictedInterval) <= recencyInterval {
+ if predicted_glucose.count == 0 {
+ return nil
+ }
+ return predicted_glucose
+ }
+ return nil
+ }
+
+ /// Disable any active workout glucose targets
+// private var cgmCalibrated = true
+// func updateCgmCalibrationState(_ calibrated: Bool) {
+// cgmCalibrated = calibrated
+// }
+//
+
+ private var lastLowNotification : (at: Date, date: Date, value: Double, carbs: Double)?
+ private let lowWarningMinutesLookAhead : Double = 30
+ private func maybeSendFutureLowNotification() throws {
+ // TODO sendGlucoseFutureLowNotifications if appropriate
+ guard let
+ glucoseTargetRange = settings.glucoseTargetRangeSchedule,
+ let carbRatioRange = carbRatioSchedule,
+ let insulinSensitivity = insulinSensitivitySchedule,
+ let predictedGlucose = predictedGlucose,
+ let lowWarningThreshold = settings.suspendThreshold?.quantity.doubleValue(for: glucoseTargetRange.unit),
+ predictedGlucose.count > 0
+ else {
+ throw LoopError.missingDataError(details: "maybeSendFutureLowNotification Loop configuration data not set", recovery: nil)
+ }
+
+
+ let currentDate = Date()
+ let unit = glucoseTargetRange.unit
+ //let overall_min = glucose.value(
+ let min = lowWarningThreshold
+ // let min = glucoseTargetRange.value(at: currentDate).minValue
+ var minDate : Date? = nil
+ var lowDate : Date? = nil
+ var minValue : Double = min
+ var lastValue : Double?
+ for p in predictedGlucose {
+ let future = p.startDate
+ let value = p.quantity.doubleValue(for: unit)
+ //let target = glucoseTargetRange.value(at: future).minValue
+ if future.timeIntervalSince(currentDate) > TimeInterval(minutes: lowWarningMinutesLookAhead) {
+ break
+ }
+
+ if value < minValue {
+ if lowDate == nil {
+ lowDate = future
+ }
+ minDate = future
+
+ minValue = round(value)
+ }
+
+ lastValue = value
+ }
+
+
+
+ let currentGlucose = predictedGlucose.first!.quantity.doubleValue(for: unit)
+ if currentGlucose > 250 {
+ if self.lastLowNotification != nil {
+ //self.addInternalNote("sendGlucoseFutureLowNotifications clear because currentGlucose > 250 \(currentGlucose)")
+ NotificationManager.clearGlucoseFutureLowNotifications()
+ self.lastLowNotification = nil
+ }
+ return
+ } else if currentGlucose < min {
+ // handled by DexCom.
+ return
+ }
+
+ if let lastValue = lastValue, lastValue > min {
+ // no warning if we eventually get over it.
+ if self.lastLowNotification != nil {
+ //self.addInternalNote("sendGlucoseFutureLowNotifications clear because lastValue > min \(lastValue) \(min)")
+ NotificationManager.clearGlucoseFutureLowNotifications()
+
+ self.lastLowNotification = nil
+ }
+ return
+ }
+
+ if let lowDate = lowDate, let minDate = minDate {
+ let target = glucoseTargetRange.value(at: minDate).minValue
+ let insulinSensitivity = insulinSensitivity.quantity(at: minDate).doubleValue(for: glucoseTargetRange.unit)
+ let carbRatio = carbRatioRange.quantity(at: minDate).doubleValue(for: HKUnit.gram())
+ let maxDelta = target - minValue
+ let carbs = maxDelta / insulinSensitivity * carbRatio
+
+ // Always round up to next 10 carbs.
+ let roundedCarbs = round((carbs + 5) / 10) * 10
+ let minutes = Int(lowDate.timeIntervalSince(currentDate) / 60)
+ if minutes > 0 {
+
+ if let lastLow = lastLowNotification, lowDate > lastLow.date, minValue > lastLow.value {
+ // too close or going up again.
+// addInternalNote("sendGlucoseFutureLowNotifications too close")
+ } else if let lastLow = lastLowNotification, lastLow.at.timeIntervalSinceNow > TimeInterval(minutes: -5) {
+ // only sent a notification once every 5 minutes.
+ } else {
+ addInternalNote("sendGlucoseFutureLowNotifications: Low in \(minutes) minutes, target: \(target), threshold: \(min), minimal glucose: \(minValue), carbs: \(roundedCarbs), last: \(String(describing: lastLowNotification))")
+
+// addInternalNote( "sendGlucoseFutureLowNotifications sent")
+ NotificationManager.sendGlucoseFutureLowNotifications(currentDate: currentDate, lowDate: lowDate, target: round(min), glucose: minValue, carbs: roundedCarbs)
+ lastLowNotification = (currentDate, lowDate, minValue, roundedCarbs)
+ }
+ }
+ } else {
+ if self.lastLowNotification != nil {
+ //addInternalNote("sendGlucoseFutureLowNotifications clear because no lowDate or minDate")
+ NotificationManager.clearGlucoseFutureLowNotifications()
+
+ self.lastLowNotification = nil
+ }
+ }
+ }
+
+ var highGlucoseEffect : [GlucoseEffect]?
+ /// An an effects for lower insulin action for high glucose values.
+ ///
+ ///
+ /// - Throws: LoopError.missingDataError if effect data isn't available
+ private func updateHighGlucoseBoost() {
+ dispatchPrecondition(condition: .onQueue(dataAccessQueue))
+
+ guard let change = lastGlucoseChange else {
+ highGlucoseEffect = nil
+ return // Expected case for calibrations
+ }
+
+ // Predict high glucose insensitivity
+ // let startDate = change.end.startDate
+ let duration = TimeInterval(minutes: 30)
+
+ let glucoseUnit = HKUnit.milligramsPerDeciliter()
+ let currentGlucose = change.end.quantity.doubleValue(for: glucoseUnit)
+
+ let velocityUnit = glucoseUnit.unitDivided(by: HKUnit.second())
+ let boostStart = 220.0
+ let boostScale = 0.5
+ let boost = (currentGlucose - boostStart) * boostScale
+
+ let averageVelocity = HKQuantity(unit: velocityUnit, doubleValue: boost / duration)
+ //let velocity = GlucoseEffectVelocity(startDate: startDate, endDate: startDate.addingTimeInterval(duration), quantity: averageVelocity)
+ let type = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodGlucose)!
+ let glucose = HKQuantitySample(type: type, quantity: change.end.quantity, start: change.end.startDate, end: change.end.endDate)
+ let effect = LoopMath.decayEffect(from: glucose, atRate: averageVelocity, for: duration)
+ print("updateHighGlucoseBoost", currentGlucose, boost, effect)
+ highGlucoseEffect = effect
+ }
+
}
@@ -965,6 +1807,8 @@ protocol LoopState {
/// The last-calculated carbs on board
var carbsOnBoard: CarbValue? { get }
+ var insulinOnBoard: InsulinValue? { get }
+
/// An error in the current state of the loop, or one that happened during the last attempt to loop.
var error: Error? { get }
@@ -977,6 +1821,14 @@ protocol LoopState {
/// The last set temp basal
var lastTempBasal: DoseEntry? { get }
+ var lastRequestedBolus: (units: Double, date: Date, reservoir: ReservoirValue?)? { get }
+
+ var pumpDetachedMode: Date? { get }
+
+ var treatmentInformation: TreatmentInformation? { get }
+
+ var validGlucose: GlucoseValue? { get }
+
/// The calculated timeline of predicted glucose values
var predictedGlucose: [GlucoseValue]? { get }
@@ -1013,6 +1865,11 @@ extension LoopDataManager {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
return loopDataManager.carbsOnBoard
}
+
+ var insulinOnBoard: InsulinValue? {
+ dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
+ return loopDataManager.insulinOnBoard
+ }
var error: Error? {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
@@ -1033,7 +1890,27 @@ extension LoopDataManager {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
return loopDataManager.lastTempBasal
}
-
+
+ var lastRequestedBolus: (units: Double, date: Date, reservoir: ReservoirValue?)? {
+ dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
+ return loopDataManager.lastRequestedBolus
+ }
+
+ var pumpDetachedMode: Date? {
+ dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
+ return loopDataManager.pumpDetachedMode
+ }
+
+ var treatmentInformation: TreatmentInformation? {
+ dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
+ return loopDataManager.getTreatmentInformation()
+ }
+
+ var validGlucose: GlucoseValue? {
+ dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
+ return loopDataManager.getValidGlucose()
+ }
+
var predictedGlucose: [GlucoseValue]? {
dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue))
return loopDataManager.predictedGlucose
@@ -1067,11 +1944,12 @@ extension LoopDataManager {
/// - Parameter manager: The loop manager
/// - Parameter state: The current state of the manager. This is invalid to access outside of the closure.
func getLoopState(_ handler: @escaping (_ manager: LoopDataManager, _ state: LoopState) -> Void) {
+ StatisticsManager.shared.inc("getLoopState")
dataAccessQueue.async {
var updateError: Error?
do {
- try self.update()
+ try self.update("getLoopState")
} catch let error {
updateError = error
}
@@ -1095,38 +1973,23 @@ extension LoopDataManager {
"## LoopDataManager",
"settings: \(String(reflecting: manager.settings))",
"insulinCounteractionEffects: \(String(reflecting: manager.insulinCounteractionEffects))",
+ "insulinOnBoard: \(String(describing: state.insulinOnBoard))",
"predictedGlucose: \(state.predictedGlucose ?? [])",
"retrospectivePredictedGlucose: \(state.retrospectivePredictedGlucose ?? [])",
"recommendedTempBasal: \(String(describing: state.recommendedTempBasal))",
"recommendedBolus: \(String(describing: state.recommendedBolus))",
- "lastBolus: \(String(describing: manager.lastRequestedBolus))",
+ "lastdBolus: \(String(describing: state.lastRequestedBolus))",
+ "pumpDetachedMode: \(String(describing: state.pumpDetachedMode))",
+ "treatmentInformation: \(String(describing: state.treatmentInformation))",
+ "validGlucose: \(String(describing: state.validGlucose))",
"lastGlucoseChange: \(String(describing: manager.lastGlucoseChange))",
"retrospectiveGlucoseChange: \(String(describing: manager.retrospectiveGlucoseChange))",
"lastLoopCompleted: \(String(describing: state.lastLoopCompleted))",
"lastTempBasal: \(String(describing: state.lastTempBasal))",
- "carbsOnBoard: \(String(describing: state.carbsOnBoard))"
+ "carbsOnBoard: \(String(describing: state.carbsOnBoard))",
+ "error: \(String(describing: state.error))"
]
- var loopError = state.error
- // TODO: this should be moved to doseStore.generateDiagnosticReport
- self.doseStore.insulinOnBoard(at: Date()) { (result) in
-
- let insulinOnBoard: InsulinValue?
-
- switch result {
- case .success(let value):
- insulinOnBoard = value
- case .failure(let error):
- insulinOnBoard = nil
-
- if loopError == nil {
- loopError = error
- }
- }
-
- entries.append("insulinOnBoard: \(String(describing: insulinOnBoard))")
- entries.append("error: \(String(describing: loopError))")
- entries.append("")
self.glucoseStore.generateDiagnosticReport { (report) in
entries.append(report)
@@ -1144,7 +2007,7 @@ extension LoopDataManager {
}
}
}
- }
+
}
}
}
@@ -1167,4 +2030,15 @@ protocol LoopDataManagerDelegate: class {
/// - completion: A closure called once on completion
/// - result: The enacted basal
func loopDataManager(_ manager: LoopDataManager, didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) -> Void
+
+ /// Informs the delegate that an immediate bolus is recommended
+ ///
+ /// - Parameters:
+ /// - manager: The manager
+ /// - basal: The new recommended bolus
+ /// - completion: A closure called once on completion
+ /// - result: The enacted bolus
+ func loopDataManager(_ manager: LoopDataManager, didRecommendBolus bolus: (recommendation: BolusRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) -> Void
+
+ func loopDataManager(_ manager: LoopDataManager, uploadTreatments treatments: [NightscoutTreatment], completion: @escaping (Result<[String]>) -> Void) -> Void
}
diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift
index a773507f84..ffee108482 100644
--- a/Loop/Managers/NightscoutDataManager.swift
+++ b/Loop/Managers/NightscoutDataManager.swift
@@ -38,6 +38,7 @@ final class NightscoutDataManager {
else {
return
}
+ StatisticsManager.shared.inc("loopDataUpdated")
deviceDataManager.loopManager.getLoopState { (manager, state) in
var loopError = state.error
diff --git a/Loop/Managers/NotificationManager.swift b/Loop/Managers/NotificationManager.swift
index 18e6ae97b9..e0a645968b 100644
--- a/Loop/Managers/NotificationManager.swift
+++ b/Loop/Managers/NotificationManager.swift
@@ -19,6 +19,9 @@ struct NotificationManager {
case pumpBatteryLow
case pumpReservoirEmpty
case pumpReservoirLow
+
+ // PRIVATE
+ case GlucoseLow
}
enum Action: String {
@@ -28,6 +31,13 @@ struct NotificationManager {
enum UserInfoKey: String {
case bolusAmount
case bolusStartDate
+
+ // PRIVATE
+ case GlucoseLowRemaining
+ case GlucoseMinTarget
+ case GlucoseLowValue
+ case GlucoseCarbSuggestion
+ case GlucoseLowDate
}
private static var notificationCategories: Set {
@@ -104,7 +114,7 @@ struct NotificationManager {
// Give a little extra time for a loop-in-progress to complete
let gracePeriod = TimeInterval(minutes: 0.5)
- for minutes: Double in [20, 40, 60, 120] {
+ for minutes: Double in [30, 60, 90, 120] {
let notification = UNMutableNotificationContent()
let failureInterval = TimeInterval(minutes: minutes)
@@ -202,3 +212,51 @@ struct NotificationManager {
UNUserNotificationCenter.current().add(request)
}
}
+
+
+// PRIVATE MODIFICATIONS
+
+extension NotificationManager {
+
+ static func clearGlucoseFutureLowNotifications() {
+ UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [Category.GlucoseLow.rawValue])
+ //UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [Category.GlucoseLow.rawValue])
+ }
+
+
+ static func sendGlucoseFutureLowNotifications(currentDate: Date, lowDate: Date, target: Double, glucose: Double, carbs: Double) {
+ let notification = UNMutableNotificationContent()
+ let minutes = Int(lowDate.timeIntervalSince(currentDate) / 60)
+ notification.title = NSLocalizedString("Future Glucose Low", comment: "The notification title for a low glucose alarm.")
+ notification.body = String(format: NSLocalizedString("Glucose likely below target of %@ in %@ minutes. Eat %@ g carbs and record them in Loop. Eventual glucose %@.", comment: "The notification alert describing a possible low glucose event. The substitution parameter is the time remaining."),
+
+ NumberFormatter.localizedString(from: NSNumber(value: target), number: .decimal),
+ NumberFormatter.localizedString(from: NSNumber(value: minutes), number: .decimal),
+ NumberFormatter.localizedString(from: NSNumber(value: carbs), number: .decimal),
+ NumberFormatter.localizedString(from: NSNumber(value: glucose), number: .decimal)
+
+ )
+ notification.sound = UNNotificationSound.default()
+
+ if lowDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) {
+ notification.categoryIdentifier = Category.GlucoseLow.rawValue
+ }
+
+ notification.userInfo = [
+
+ UserInfoKey.GlucoseMinTarget.rawValue: target,
+ UserInfoKey.GlucoseLowValue.rawValue: glucose,
+ UserInfoKey.GlucoseCarbSuggestion.rawValue: carbs,
+ UserInfoKey.GlucoseLowDate.rawValue: lowDate
+ ]
+
+ let request = UNNotificationRequest(
+ // Only support 1 low notification at once
+ identifier: Category.GlucoseLow.rawValue,
+ content: notification,
+ trigger: nil
+ )
+
+ UNUserNotificationCenter.current().add(request)
+ }
+}
diff --git a/Loop/Managers/StatisticsManager.swift b/Loop/Managers/StatisticsManager.swift
new file mode 100644
index 0000000000..eb388749da
--- /dev/null
+++ b/Loop/Managers/StatisticsManager.swift
@@ -0,0 +1,46 @@
+//
+// AnalyticsManager.swift
+// Naterade
+//
+// Created by Nathan Racklyeft on 4/28/16.
+// Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+
+final class StatisticsManager: IdentifiableClass {
+
+ var stats : [String:Int]
+ var lastLog : Date
+
+ init() {
+ stats = [:]
+ lastLog = Date()
+ }
+
+ static let shared = StatisticsManager()
+
+ public var loopManager : LoopDataManager? = nil
+
+ private func str() -> String {
+ return "Stats since \(lastLog): \(stats)"
+ }
+
+ public func inc(_ name: String) {
+ // This should use a queue.
+ if let i = stats[name] {
+ stats[name] = i + 1
+ } else {
+ stats[name] = 1
+ }
+ NSLog(str())
+ if lastLog.timeIntervalSinceNow < TimeInterval(hours: -1) {
+ if let loop = self.loopManager {
+ loop.addInternalNote(str())
+ lastLog = Date()
+ stats = [:]
+ }
+ }
+ }
+}
diff --git a/Loop/Models/BolusRecommendation.swift b/Loop/Models/BolusRecommendation.swift
index 8f4fb585bb..c12ca63081 100644
--- a/Loop/Models/BolusRecommendation.swift
+++ b/Loop/Models/BolusRecommendation.swift
@@ -15,6 +15,7 @@ enum BolusRecommendationNotice {
case glucoseBelowSuspendThreshold(minGlucose: GlucoseValue)
case currentGlucoseBelowTarget(glucose: GlucoseValue)
case predictedGlucoseBelowTarget(minGlucose: GlucoseValue)
+ case carbOnly(carbs: Double, originalAmount: Double?)
}
@@ -38,7 +39,14 @@ extension BolusRecommendationNotice {
let glucoseFormatter = NumberFormatter.glucoseFormatter(for: unit)
let minBGStr = glucoseFormatter.describingGlucose(minGlucose.quantity, for: unit)!
return String(format: NSLocalizedString("Predicted glucose at %1$@ is %2$@.", comment: "Message when offering bolus recommendation even though bg is below range and minBG is in future. (1: glucose time)(2: glucose number)"), time, minBGStr)
-
+ case .carbOnly(carbs: let carbs, originalAmount: let originalAmount):
+ let carbsRounded = round(carbs)
+ if let amount = originalAmount {
+ return String(format: NSLocalizedString("No glucose, recommendation based on \(carbsRounded) g of carbs since last bolus. Exceeds maximum Bolus, was \(amount) Units!", comment: "Notice message when recommending bolus when no glucose is available. (1: carb amount in gram. 2: original amount of Insulin.)"))
+ } else {
+ return String(format: NSLocalizedString("No glucose, recommendation based on \(carbsRounded) g of carbs since last bolus.", comment: "Notice message when recommending bolus when no glucose is available. (1: carb amount in gram)"))
+ }
+
}
}
}
@@ -51,7 +59,9 @@ extension BolusRecommendationNotice: Equatable {
case (.currentGlucoseBelowTarget, .currentGlucoseBelowTarget):
return true
-
+ case (let .carbOnly(carbs1), let .carbOnly(carbs2)):
+ return carbs1 == carbs2
+
case (let .predictedGlucoseBelowTarget(minGlucose1), let .predictedGlucoseBelowTarget(minGlucose2)):
// GlucoseValue is not equatable
return
@@ -67,24 +77,31 @@ extension BolusRecommendationNotice: Equatable {
struct BolusRecommendation {
let amount: Double
+ let netAmount: Double // can be negative, e.g. to calculate recommended carbs.
let pendingInsulin: Double
var notice: BolusRecommendationNotice?
+ // only in case of negative amount
+ let target : HKQuantity?
+ let minPrediction : GlucoseValue?
- init(amount: Double, pendingInsulin: Double, notice: BolusRecommendationNotice? = nil) {
+ init(amount: Double, pendingInsulin: Double, notice: BolusRecommendationNotice? = nil, netAmount: Double? = nil, target: HKQuantity? = nil, minPrediction: GlucoseValue? = nil) {
self.amount = amount
+ self.netAmount = netAmount ?? amount
self.pendingInsulin = pendingInsulin
self.notice = notice
+ self.target = target
+ self.minPrediction = minPrediction
}
}
extension BolusRecommendation: Comparable {
static func ==(lhs: BolusRecommendation, rhs: BolusRecommendation) -> Bool {
- return lhs.amount == rhs.amount
+ return lhs.amount == rhs.amount && lhs.netAmount == rhs.netAmount
}
static func <(lhs: BolusRecommendation, rhs: BolusRecommendation) -> Bool {
- return lhs.amount < rhs.amount
+ return lhs.amount < rhs.amount && lhs.netAmount < rhs.netAmount
}
}
diff --git a/Loop/Models/GitVersionInformation.swift b/Loop/Models/GitVersionInformation.swift
new file mode 100644
index 0000000000..42f112e1fd
--- /dev/null
+++ b/Loop/Models/GitVersionInformation.swift
@@ -0,0 +1,26 @@
+//
+// GitVersionInformation.swift
+// Loop2
+//
+// Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+struct GitVersionInformation {
+ let dict : [String:String]
+}
+
+extension GitVersionInformation {
+ init() {
+ var myDict: [String: String]?
+ if let path = Bundle.main.path(forResource: "GitInfo", ofType: "plist") {
+ myDict = NSDictionary(contentsOfFile: path) as? [String: String]
+ }
+ if let d = myDict {
+ dict = d
+ } else {
+ dict = [:]
+ }
+ }
+}
diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift
index 24d374a153..1d40bdf9cc 100644
--- a/Loop/Models/LoopSettings.swift
+++ b/Loop/Models/LoopSettings.swift
@@ -12,6 +12,8 @@ import RileyLinkBLEKit
struct LoopSettings {
var dosingEnabled = false
+ var bolusEnabled = false
+
let dynamicCarbAbsorptionEnabled = true
var glucoseTargetRangeSchedule: GlucoseRangeSchedule?
@@ -19,10 +21,24 @@ struct LoopSettings {
var maximumBasalRatePerHour: Double?
var maximumBolus: Double?
+
+ var maximumInsulinOnBoard: Double?
var suspendThreshold: GlucoseThreshold? = nil
var retrospectiveCorrectionEnabled = true
+
+ // Not configurable through UI
+ let automatedBolusThreshold: Double = 0.1
+ let automatedBolusRatio: Double = 1.0
+ let automaticBolusInterval: TimeInterval = TimeInterval(minutes: 6)
+ let absorptionRate: Double = 20
+
+ let minimumRecommendedBolus: Double = 0.2
+ let insulinIncrementPerUnit: Double = 10 // 0.1 steps in basal and bolus
+
+ let absorptionTimeOverrun = 1.25
+ let absorptionTimeMultiplier = 0.8
}
@@ -58,6 +74,10 @@ extension LoopSettings: RawRepresentable {
if let dosingEnabled = rawValue["dosingEnabled"] as? Bool {
self.dosingEnabled = dosingEnabled
}
+
+ if let bolusEnabled = rawValue["bolusEnabled"] as? Bool {
+ self.bolusEnabled = bolusEnabled
+ }
if let rawValue = rawValue["glucoseTargetRangeSchedule"] as? GlucoseRangeSchedule.RawValue {
self.glucoseTargetRangeSchedule = GlucoseRangeSchedule(rawValue: rawValue)
@@ -65,6 +85,7 @@ extension LoopSettings: RawRepresentable {
self.maximumBasalRatePerHour = rawValue["maximumBasalRatePerHour"] as? Double
+ self.maximumInsulinOnBoard = rawValue["maximumInsulinOnBoard"] as? Double
self.maximumBolus = rawValue["maximumBolus"] as? Double
if let rawThreshold = rawValue["minimumBGGuard"] as? GlucoseThreshold.RawValue {
@@ -80,11 +101,13 @@ extension LoopSettings: RawRepresentable {
var raw: RawValue = [
"version": LoopSettings.version,
"dosingEnabled": dosingEnabled,
+ "bolusEnabled": bolusEnabled,
"retrospectiveCorrectionEnabled": retrospectiveCorrectionEnabled
]
raw["glucoseTargetRangeSchedule"] = glucoseTargetRangeSchedule?.rawValue
raw["maximumBasalRatePerHour"] = maximumBasalRatePerHour
+ raw["maximumInsulinOnBoard"] = maximumInsulinOnBoard
raw["maximumBolus"] = maximumBolus
raw["minimumBGGuard"] = suspendThreshold?.rawValue
diff --git a/Loop/Models/ServiceAuthentication/LogglyService.swift b/Loop/Models/ServiceAuthentication/LogglyService.swift
index f233bbca53..e07c92981e 100644
--- a/Loop/Models/ServiceAuthentication/LogglyService.swift
+++ b/Loop/Models/ServiceAuthentication/LogglyService.swift
@@ -77,9 +77,13 @@ enum LogglyAPIEndpoint: String {
return "https://logs-01.loggly.com/\(rawValue)/"
}
- func url(token: String, tags: [String]) -> URL {
+ func url(token: String, tags: [String]) -> URL? {
let tags = tags.count > 0 ? tags : ["http"]
- return URL(string: "\(base)\(token)/tag/\(tags.joined(separator: ","))/")!
+ let cleanToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
+ if let url = URL(string: "\(base)\(cleanToken)/tag/\(tags.joined(separator: ","))/") {
+ return url
+ }
+ return nil
}
}
@@ -96,7 +100,10 @@ extension URLSession {
}
private func inputTask(body: Data, contentType: String, token: String, tags: [String]) -> URLSessionUploadTask? {
- let url = LogglyAPIEndpoint.inputs.url(token: token, tags: tags)
+ guard let url = LogglyAPIEndpoint.inputs.url(token: token, tags: tags) else {
+ return nil
+ }
+
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(contentType, forHTTPHeaderField: "content-type")
diff --git a/Loop/Models/TreatmentInformation.swift b/Loop/Models/TreatmentInformation.swift
new file mode 100644
index 0000000000..2699e25bfc
--- /dev/null
+++ b/Loop/Models/TreatmentInformation.swift
@@ -0,0 +1,123 @@
+//
+// Loop
+//
+// Created by Erik on 9/1/17.
+// Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import CarbKit
+import HealthKit
+import InsulinKit
+import LoopKit
+import MinimedKit
+import HealthKit
+import GlucoseKit
+import RileyLinkKit
+
+
+struct TreatmentInformation {
+ enum BolusState : String {
+ case none // none given
+ case prohibited // not allowed
+ case recommended // recommendation
+ // command sent to pump
+ case sent
+ // initial states
+ case pending
+ case maybefailed
+ // result
+ case failed
+ case success
+ case timeout
+ }
+ var state : BolusState = .none
+ var units : Double = 0.0
+ var carbs : Double = 0.0
+ var date : Date
+ var sent : Date?
+ // if a new bolus is allowed
+ var allowed : Bool = false
+ var message : String = ""
+
+ var reservoir: ReservoirValue? = nil
+ var attempts: Int? = 0
+
+ func equal(_ other: TreatmentInformation) -> Bool {
+ return state == other.state && date == other.date && units == other.units && message == other.message && allowed == other.allowed
+ }
+
+ func inProgress() -> Bool {
+ return state == .pending || state == .maybefailed || state == .sent
+ }
+
+ func description() -> String {
+ switch state {
+ case .none: return ""
+ case .prohibited: return "Prohibited"
+ case .recommended: return "Recommended"
+
+ case .sent: return "Pending"
+ case .pending: return "Delivering"
+
+ case .maybefailed: return "Maybe failed"
+
+ case .failed: return "Failed"
+ case .success: return "Successful"
+ case .timeout: return "Timed out"
+ }
+ }
+
+ func kind() -> String {
+ if state == .recommended && carbs > 0 {
+ return "Carbs"
+ }
+ return "Bolus"
+ }
+
+ func explanation(bolusEnabled: Bool = true) -> String {
+ var val = ""
+ switch state {
+ case .none:
+ val = ""
+ case .prohibited:
+ val = "Data old, pump in reach?"
+ case .recommended:
+ if units > 0 {
+ let _ = bolusEnabled
+ /*if bolusEnabled {
+ val = "Will be automatically given."
+ } else {*/
+ // In the carb only case this determination is not correct.
+ // Rather be safe and tell the user to tap, but have their back
+ // if they don't.
+ val = "Tap to deliver now."
+ //}
+ } else if carbs > 0 {
+ let i = Int(carbs)
+ val = "Eat \(i) g fast acting carbs like juice, glucose tabs, etc."
+ }
+ case .sent:
+ val = "Sending command to pump."
+ case .pending:
+ val = "(can turn phone off)."
+ case .maybefailed:
+ val = "Check pump!"
+
+ case .failed:
+ val = "Pump in reach?"
+ case .success:
+ val = "Success!"
+ case .timeout:
+ val = "Timeout - Check pump!"
+
+ }
+ if message != "" {
+ val = "\(val) - \(message)"
+ }
+ if let a = attempts, a > 1 {
+ val = "\(val) [#\(a)]"
+ }
+ return val
+ }
+}
diff --git a/Loop/Sounds/beep1.wav b/Loop/Sounds/beep1.wav
new file mode 100644
index 0000000000..725a86ba05
Binary files /dev/null and b/Loop/Sounds/beep1.wav differ
diff --git a/Loop/Sounds/beep2.wav b/Loop/Sounds/beep2.wav
new file mode 100644
index 0000000000..3b666201cc
Binary files /dev/null and b/Loop/Sounds/beep2.wav differ
diff --git a/Loop/Sounds/beep3.wav b/Loop/Sounds/beep3.wav
new file mode 100644
index 0000000000..e27327269d
Binary files /dev/null and b/Loop/Sounds/beep3.wav differ
diff --git a/Loop/Sounds/ding1.wav b/Loop/Sounds/ding1.wav
new file mode 100644
index 0000000000..5ab1ea5336
Binary files /dev/null and b/Loop/Sounds/ding1.wav differ
diff --git a/Loop/Sounds/ding2.wav b/Loop/Sounds/ding2.wav
new file mode 100644
index 0000000000..f33add8d96
Binary files /dev/null and b/Loop/Sounds/ding2.wav differ
diff --git a/Loop/Sounds/ding3.wav b/Loop/Sounds/ding3.wav
new file mode 100644
index 0000000000..968ac348f9
Binary files /dev/null and b/Loop/Sounds/ding3.wav differ
diff --git a/Loop/Sounds/error.wav b/Loop/Sounds/error.wav
new file mode 100644
index 0000000000..fcad3cf9bb
Binary files /dev/null and b/Loop/Sounds/error.wav differ
diff --git a/Loop/Sounds/future_low.wav b/Loop/Sounds/future_low.wav
new file mode 100644
index 0000000000..fcad3cf9bb
Binary files /dev/null and b/Loop/Sounds/future_low.wav differ
diff --git a/Loop/View Controllers/BolusViewController+LoopDataManager.swift b/Loop/View Controllers/BolusViewController+LoopDataManager.swift
index 84246ef882..a3ef0e7bff 100644
--- a/Loop/View Controllers/BolusViewController+LoopDataManager.swift
+++ b/Loop/View Controllers/BolusViewController+LoopDataManager.swift
@@ -10,10 +10,10 @@ import HealthKit
extension BolusViewController {
- func configureWithLoopManager(_ manager: LoopDataManager, recommendation: BolusRecommendation?, glucoseUnit: HKUnit) {
+ func configureWithLoopManager(_ manager: LoopDataManager, recommendation: BolusRecommendation?, glucoseUnit: HKUnit, expertMode: Bool) {
manager.getLoopState { (manager, state) in
let maximumBolus = manager.settings.maximumBolus
-
+ let maxInsulinOnBoard = manager.settings.maximumInsulinOnBoard
let activeCarbohydrates = state.carbsOnBoard?.quantity.doubleValue(for: .gram())
let bolusRecommendation: BolusRecommendation?
@@ -22,28 +22,23 @@ extension BolusViewController {
} else {
bolusRecommendation = state.recommendedBolus?.recommendation
}
-
- manager.doseStore.insulinOnBoard(at: Date()) { (result) in
- let activeInsulin: Double?
-
- switch result {
- case .success(let value):
- activeInsulin = value.value
- case .failure:
- activeInsulin = nil
- }
+
+ let iob = state.insulinOnBoard
DispatchQueue.main.async {
if let maxBolus = maximumBolus {
self.maxBolus = maxBolus
}
+ self.maximumInsulinOnBoard = maxInsulinOnBoard
self.glucoseUnit = glucoseUnit
- self.activeInsulin = activeInsulin
+ self.activeInsulin = iob?.value
self.activeCarbohydrates = activeCarbohydrates
self.bolusRecommendation = bolusRecommendation
+
+ self.expertMode = expertMode
}
- }
+
}
}
}
diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift
index 58d01a89c2..34cd4859e6 100644
--- a/Loop/View Controllers/BolusViewController.swift
+++ b/Loop/View Controllers/BolusViewController.swift
@@ -69,6 +69,7 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex
didSet {
let amount = bolusRecommendation?.amount ?? 0
recommendedBolusAmountLabel?.text = bolusUnitsFormatter.string(from: NSNumber(value: amount))
+ acceptRecommendedBolus()
updateNotice()
if let pendingInsulin = bolusRecommendation?.pendingInsulin {
self.pendingInsulin = pendingInsulin
@@ -114,8 +115,14 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex
}
}
-
+ var maximumInsulinOnBoard: Double? = nil
var maxBolus: Double = 25
+
+ var expertMode: Bool = false {
+ didSet {
+ bolusAmountTextField?.isEnabled = expertMode
+ }
+ }
private(set) var bolus: Double?
@@ -172,35 +179,82 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex
@IBOutlet weak var bolusAmountTextField: UITextField!
// MARK: - Actions
-
+ private func roundedBolus(_ bolus: Double) -> Double {
+ return round(bolus * 10) / 10
+ }
+
@IBAction func authenticateBolus(_ sender: Any) {
bolusAmountTextField.resignFirstResponder()
- guard let text = bolusAmountTextField?.text, let bolus = bolusUnitsFormatter.number(from: text)?.doubleValue,
- let amountString = bolusUnitsFormatter.string(from: NSNumber(value: bolus)) else {
+ guard let text = bolusAmountTextField?.text, let bolus = bolusUnitsFormatter.number(from: text)?.doubleValue else {
+ NSLog("BolusViewController - empty")
return
}
-
+
+ let rounded = roundedBolus(bolus)
+ guard rounded == bolus else {
+ NSLog("BolusViewController - rounded")
+ bolusAmountTextField?.text = "\(rounded)"
+ presentAlertController(withTitle: NSLocalizedString("Rounded Bolus", comment: "The title of the alert describing a rounded bolus validation error"), message: String(format: NSLocalizedString("The bolus amount has to be a multiple of 0.1, please try again.", comment: "Body of the alert describing a rounding bolus validation error.")))
+ return
+ }
+
guard bolus <= maxBolus else {
+ NSLog("BolusViewController - maxBolus")
presentAlertController(withTitle: NSLocalizedString("Exceeds Maximum Bolus", comment: "The title of the alert describing a maximum bolus validation error"), message: String(format: NSLocalizedString("The maximum bolus amount is %@ Units", comment: "Body of the alert describing a maximum bolus validation error. (1: The localized max bolus value)"), bolusUnitsFormatter.string(from: NSNumber(value: maxBolus)) ?? ""))
return
}
-
- let context = LAContext()
-
- if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
- context.evaluatePolicy(.deviceOwnerAuthentication,
- localizedReason: String(format: NSLocalizedString("Authenticate to Bolus %@ Units", comment: "The message displayed during a device authentication prompt for bolus specification"), amountString),
- reply: { (success, error) in
- if success {
- DispatchQueue.main.async {
- self.setBolusAndClose(bolus)
- }
+
+ guard bolus >= 0 else {
+ NSLog("BolusViewController - negative")
+ presentAlertController(withTitle: NSLocalizedString("Negative Bolus", comment: "The title of the alert describing a negative bolus validation error"), message: String(format: NSLocalizedString("The bolus amount is negative", comment: "Body of the alert describing a negative bolus validation error.")))
+ return
+ }
+
+ if let maxIOB = maximumInsulinOnBoard, maxIOB > 0 {
+ guard bolus + (activeInsulin ?? 0) <= maxIOB else {
+ NSLog("BolusViewController - maxIOB")
+ presentAlertController(withTitle: NSLocalizedString("Would exceed Maximum Insulin on Board", comment: "The title of the alert describing a maximum insulin on board validation error"), message: String(format: NSLocalizedString("The insulin on board amount is %@ Units", comment: "Body of the alert describing a maximum iob validation error. (1: The localized max iob value)"),
+ bolusUnitsFormatter.string(from: NSNumber(value: maxIOB)) ?? ""))
+ return
+ }
+ }
+
+ let recommendation = bolusRecommendation?.amount ?? 0
+ guard bolus <= recommendation else {
+ NSLog("BolusViewController - exceeds")
+
+ //1. Create the alert controller.
+ let alert = UIAlertController(title: "Exceeds Recommended Bolus", message: "The bolus amount of \(bolus) U is higher than the recommended amount of \(recommendation) U. Please re-enter the amount to confirm.", preferredStyle: .alert)
+
+ //2. Add the text field. You can configure it however you need.
+ alert.addTextField { (textField) in
+ textField.text = ""
+ textField.keyboardType = UIKeyboardType.decimalPad
+ textField.autocorrectionType = UITextAutocorrectionType.no
+ }
+ // 3. Grab the value from the text field, and print it when the user clicks OK.
+ alert.addAction(UIAlertAction(title: "Deliver", style: .default, handler: { [weak alert] (_) in
+ //let result = alert?.textFields![0].text // Force unwrapping because we know it exists.
+ let wanted = "\(bolus)"
+ let result = alert?.textFields![0].text
+ if result != nil && result! == wanted {
+ self.setBolusAndClose(bolus)
+ } else {
+ self.presentAlertController(withTitle: NSLocalizedString("Exceeds Recommended Bolus", comment: "The title of the alert describing a recommended bolus validation error"), message: String(format: NSLocalizedString("The Validation failed (Recommended \(recommendation), wanted \(wanted), entered \(result!))", comment: "Body of the alert describing a recommended bolus validation error. (1: The localized recommended bolus value)")))
+ return
}
- })
- } else {
- setBolusAndClose(bolus)
+ }))
+
+ alert.addAction(UIAlertAction(title: "Back", style: .default, handler: nil))
+
+ // 4. Present the alert.
+ self.present(alert, animated: true, completion: nil)
+
+ return
}
+
+ setBolusAndClose(bolus)
}
private func setBolusAndClose(_ bolus: Double) {
diff --git a/Loop/View Controllers/CarbAbsorptionViewController.swift b/Loop/View Controllers/CarbAbsorptionViewController.swift
index 6178a26104..182674b7c8 100644
--- a/Loop/View Controllers/CarbAbsorptionViewController.swift
+++ b/Loop/View Controllers/CarbAbsorptionViewController.swift
@@ -489,7 +489,8 @@ final class CarbAbsorptionViewController: ChartsTableViewController, Identifiabl
case let vc as BolusViewController:
vc.configureWithLoopManager(self.deviceManager.loopManager,
recommendation: sender as? BolusRecommendation,
- glucoseUnit: self.charts.glucoseUnit
+ glucoseUnit: self.charts.glucoseUnit,
+ expertMode: true
)
case let vc as CarbEntryEditViewController:
if let selectedCell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: selectedCell), indexPath.row < carbStatuses.count {
diff --git a/Loop/View Controllers/CommandResponseViewController.swift b/Loop/View Controllers/CommandResponseViewController.swift
index 5fe9ba345b..06837b631f 100644
--- a/Loop/View Controllers/CommandResponseViewController.swift
+++ b/Loop/View Controllers/CommandResponseViewController.swift
@@ -18,6 +18,7 @@ extension CommandResponseViewController {
completionHandler([
"Use the Share button above save this diagnostic report to aid investigating your problem. Issues can be filed at https://github.com/LoopKit/Loop/issues.",
"Generated: \(Date())",
+ "GitVersionInformation: \(GitVersionInformation().dict)",
"",
String(reflecting: dataManager),
"",
diff --git a/Loop/View Controllers/FoodPickerCameraViewController.swift b/Loop/View Controllers/FoodPickerCameraViewController.swift
new file mode 100644
index 0000000000..ea47b90c0c
--- /dev/null
+++ b/Loop/View Controllers/FoodPickerCameraViewController.swift
@@ -0,0 +1,316 @@
+import UIKit
+import AVFoundation
+
+class FoodPickerCameraViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate, AVCapturePhotoCaptureDelegate, IdentifiableClass {
+
+ var previewLayer : AVCaptureVideoPreviewLayer?
+ @IBOutlet weak var previewView: UIView!
+
+ // If we find a device we'll store it here for later use
+ private var captureDevice : AVCaptureDevice?
+ private let captureSession = AVCaptureSession()
+ private var output = AVCapturePhotoOutput()
+ private var videoOutput = AVCaptureVideoDataOutput()
+ private var videoDataOutputQueue = DispatchQueue.init(label: "VideoDataOutputQueue")
+ private var captureQueue = DispatchQueue.init(label: "PhotoCaptureSessionQueue")
+
+
+ var selectedPath : IndexPath?
+ var imageOutput : UIImage?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+
+ let cameraPermissionStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)
+
+ switch cameraPermissionStatus {
+ case .authorized:
+ NSLog("Already Authorized")
+ case .denied:
+ NSLog("denied")
+ case .restricted:
+ NSLog("restricted")
+ default:
+ AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: {
+ // [weak self]
+ (granted :Bool) -> Void in
+ NSLog("granted", granted)
+ });
+ }
+
+ let session = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: AVCaptureDevice.Position.back)
+ // Loop through all the capture devices on this phone
+ captureDevice = session.devices.first
+ NSLog("captureDevice \(String(describing: captureDevice)) \(session.devices)")
+ captureQueue.async {
+ NSLog("async beginSession")
+ self.beginSession()
+ }
+ }
+
+
+ @IBAction func captureButton(_ sender: Any) {
+ capturePhoto()
+ }
+
+ override func touchesEnded(_ touches: Set, with event: UIEvent?) {
+ capturePhoto()
+ }
+
+ func configureDevice() {
+ if let device = captureDevice {
+ do {
+ try device.lockForConfiguration()
+ //device.focusMode = .locked
+ //device.automaticallyEnablesLowLightBoostWhenAvailable = true
+ //device.automaticallyAdjustsVideoHDREnabled = true
+
+ device.unlockForConfiguration()
+ } catch let error {
+ NSLog("lockForConfiguration Failed \(error)")
+ }
+ }
+
+ }
+
+ func imageFromSampleBuffer(sampleBuffer : CMSampleBuffer) -> UIImage
+ {
+ // Get a CMSampleBuffer's Core Video image buffer for the media data
+ let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
+ // Lock the base address of the pixel buffer
+ CVPixelBufferLockBaseAddress(imageBuffer!, CVPixelBufferLockFlags.readOnly);
+
+
+ // Get the number of bytes per row for the pixel buffer
+ let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer!);
+
+ // Get the number of bytes per row for the pixel buffer
+ let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer!);
+ // Get the pixel buffer width and height
+ let width = CVPixelBufferGetWidth(imageBuffer!);
+ let height = CVPixelBufferGetHeight(imageBuffer!);
+
+ // Create a device-dependent RGB color space
+ let colorSpace = CGColorSpaceCreateDeviceRGB();
+
+ // Create a bitmap graphics context with the sample buffer data
+ var bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue
+ bitmapInfo |= CGImageAlphaInfo.premultipliedFirst.rawValue & CGBitmapInfo.alphaInfoMask.rawValue
+ //let bitmapInfo: UInt32 = CGBitmapInfo.alphaInfoMask.rawValue
+ let context = CGContext.init(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)
+ // Create a Quartz image from the pixel data in the bitmap graphics context
+ let quartzImage = context?.makeImage();
+ // Unlock the pixel buffer
+ CVPixelBufferUnlockBaseAddress(imageBuffer!, CVPixelBufferLockFlags.readOnly);
+
+ // Create an image object from the Quartz image
+ let image = UIImage.init(cgImage: quartzImage!)
+
+ return (image);
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ captureQueue.async {
+ if self.captureSession.isRunning {
+ self.captureSession.stopRunning()
+ }
+ }
+ }
+
+ private var captureVideoFrame = false
+ func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
+ if captureVideoFrame {
+ captureVideoFrame = false
+ let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer)
+ NSLog("capture sampleBuffer")
+ imageOutput = image
+ performSegue(withIdentifier: "close", sender: self)
+ }
+ }
+
+ @objc func sessionRuntimeError(notification: NSNotification) {
+ guard let errorValue = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError else {
+ return
+ }
+
+ let error = AVError(_nsError: errorValue)
+ NSLog("Capture session runtime error: \(error)")
+ }
+
+ func beginSession() {
+
+ configureDevice()
+ captureSession.beginConfiguration()
+ // Do any additional setup after loading the view, typically from a nib.
+ captureSession.sessionPreset = AVCaptureSession.Preset.photo
+
+ do {
+ if let device = captureDevice {
+ try captureSession.addInput(AVCaptureDeviceInput(device: device))
+ }
+ } catch let error {
+ NSLog("addInput Failed \(error)")
+ captureSession.commitConfiguration()
+ return
+ }
+ if captureSession.canAddOutput(output) {
+ captureSession.addOutput(output)
+ } else {
+ NSLog("Cannot add Output")
+ captureSession.commitConfiguration()
+ return
+ }
+
+ videoOutput.alwaysDiscardsLateVideoFrames = true
+ //videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey:Int(kCVPixelFormatType_32BGRA)]
+ videoOutput.videoSettings = NSDictionary(object: NSNumber(value: kCVPixelFormatType_32BGRA), forKey: NSString(string: kCVPixelBufferPixelFormatTypeKey)) as! [String:Any]
+
+ videoOutput.setSampleBufferDelegate(self, queue:self.videoDataOutputQueue)
+ //captureSession.addOutput(videoOutput)
+
+ captureSession.commitConfiguration()
+
+ let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
+ // previewLayer.videoGravity = AVLayerVideoGravityResizeAspect
+ previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
+ self.previewView.layer.masksToBounds = true
+ //previewLayer.frame = self.previewView.layer.frame
+ DispatchQueue.main.async {
+ //previewLayer.frame = self.previewView.bounds
+ previewLayer.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
+ previewLayer.bounds = previewLayer.frame
+
+ NSLog("preview layer frame \(previewLayer.frame), \(self.previewView.bounds), \(self.previewView.layer.frame)")
+ self.previewView.layer.addSublayer(previewLayer)
+ }
+ NotificationCenter.default.addObserver(self, selector: #selector(sessionRuntimeError), name: .AVCaptureSessionRuntimeError, object: captureSession)
+
+ captureSession.startRunning()
+ if !captureSession.isRunning {
+ NSLog("Cannot start capture session for some reason.")
+ }
+
+
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ //code
+ }
+
+ func capturePhoto() {
+ //captureVideoFrame = true
+ if !captureSession.isRunning {
+ return
+ }
+ let settings = AVCapturePhotoSettings()
+ let previewPixelType = settings.availablePreviewPhotoPixelFormatTypes.first!
+ let previewFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPixelType,
+ kCVPixelBufferWidthKey as String: 160,
+ kCVPixelBufferHeightKey as String: 160]
+ settings.previewPhotoFormat = previewFormat
+ output.capturePhoto(with: settings, delegate: self)
+ }
+
+ //MARK: - Add image to Library
+ @objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
+ NSLog("Successfully saved image to camera roll")
+ }
+
+ func crop(image: UIImage, withWidth width: Double, andHeight height: Double) -> UIImage? {
+
+ if let cgImage = image.cgImage {
+
+ let contextImage: UIImage = UIImage(cgImage: cgImage)
+
+ let contextSize: CGSize = contextImage.size
+
+ var posX: CGFloat = 0.0
+ var posY: CGFloat = 0.0
+ var cgwidth: CGFloat = CGFloat(width)
+ var cgheight: CGFloat = CGFloat(height)
+
+ // See what size is longer and create the center off of that
+ if contextSize.width > contextSize.height {
+ posX = 0//((contextSize.width - contextSize.height) / 2)
+ posY = 0
+ cgwidth = contextSize.height
+ cgheight = contextSize.height
+ } else {
+ posX = 0
+ posY = 0// ((contextSize.height - contextSize.width) / 2)
+ cgwidth = contextSize.width
+ cgheight = contextSize.width
+ }
+
+ let rect: CGRect = CGRect(x: posX, y: posY, width: cgwidth, height: cgheight)
+
+ // Create bitmap image from context using the rect
+ var croppedContextImage: CGImage? = nil
+ if let contextImage = contextImage.cgImage {
+ if let croppedImage = contextImage.cropping(to: rect) {
+ croppedContextImage = croppedImage
+ }
+ }
+
+ // Create a new image based on the imageRef and rotate back to the original orientation
+ if let croppedImage:CGImage = croppedContextImage {
+ let image: UIImage = UIImage(cgImage: croppedImage, scale: image.scale, orientation: image.imageOrientation)
+ return image
+ }
+
+ }
+ return nil
+ }
+
+ func photoOutput(_ output: AVCapturePhotoOutput,
+ didFinishProcessingPhoto photo: AVCapturePhoto,
+ error: Error?) {
+ if let error = error {
+ NSLog("capture error \(error.localizedDescription)")
+ return
+ }
+
+ if let data = photo.fileDataRepresentation() {
+ if let image = UIImage(data: data) {
+ saveImageCallback(image)
+ }
+ }
+ }
+ /*
+ func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
+
+ if let error = error {
+ NSLog("capture error \(error.localizedDescription)")
+ return
+ }
+
+ if let sampleBuffer = photoSampleBuffer, let previewBuffer = previewPhotoSampleBuffer, let dataImage = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: sampleBuffer, previewPhotoSampleBuffer: previewBuffer), let image = UIImage(data: dataImage) {
+
+ saveImageCallback(image)
+ }
+ }
+ */
+
+ func saveImageCallback(_ image: UIImage) {
+ NSLog("Image Size \(image.size)")
+ let newWidth = 1024.0
+ let newHeight = 1024.0
+ let newSize = CGSize(width: newWidth, height: newHeight)
+ let renderer = UIGraphicsImageRenderer(size: newSize)
+
+ let croppedImage : UIImage? = self.crop(image: image, withWidth: 1024, andHeight: 1024)
+
+ let newImage = renderer.image{_ in
+ croppedImage?.draw(in: CGRect.init(origin: CGPoint.zero, size: newSize))
+ }
+
+ // until we can upload the images, just save them to the camera roll.
+ UIImageWriteToSavedPhotosAlbum(newImage, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
+
+ imageOutput = newImage
+
+ performSegue(withIdentifier: "close", sender: self)
+ }
+
+}
diff --git a/Loop/View Controllers/FoodPickerViewController.swift b/Loop/View Controllers/FoodPickerViewController.swift
new file mode 100644
index 0000000000..85883df0d5
--- /dev/null
+++ b/Loop/View Controllers/FoodPickerViewController.swift
@@ -0,0 +1,352 @@
+//
+// FoodPickerViewController.swift
+// Loop
+//
+// Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+final class FoodPickerFlowLayout: UICollectionViewFlowLayout {
+
+ override init() {
+ super.init()
+ setupLayout()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupLayout()
+ }
+
+ func setupLayout() {
+ minimumInteritemSpacing = 1
+ minimumLineSpacing = 1
+ scrollDirection = .vertical
+ headerReferenceSize = CGSize(width: 0, height: 40)
+ }
+
+ override var itemSize: CGSize {
+ set {
+
+ }
+ get {
+ let numberOfColumns: CGFloat = 3
+
+ let itemWidth = (self.collectionView!.frame.width - (numberOfColumns - 1)) / numberOfColumns
+ return CGSize(width: itemWidth, height: itemWidth)
+ }
+ }
+}
+
+final class NewFoodPickerCollectionView : UICollectionView {
+
+}
+
+final class NewFoodPickerViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, IdentifiableClass {
+
+ @IBOutlet weak var collectionView: UICollectionView!
+ @IBOutlet weak var foodInfo: UILabel!
+ @IBOutlet weak var sliderMultiplier: UISlider!
+ @IBOutlet weak var selectionMultiplier: UISegmentedControl!
+ @IBOutlet weak var sliderInfo: UILabel!
+
+ var foodManager : FoodManager? = nil
+ private var selected : IndexPath? = nil
+ private var ratio : Double = 0
+
+ private var previewImage : UIImage? = nil
+ private var previewFileName : String? = nil
+
+ var foodPick : FoodPick? = nil
+ @IBOutlet weak var saveButtonItem: UIBarButtonItem!
+
+ @IBAction func saveButton(_ sender: Any) {
+ if let selected = self.selected, let item = foodItemForPath(selected) {
+ var imageIdentifier : String? = nil
+ if item.title == "Photo" {
+ imageIdentifier = previewFileName
+ }
+ NSLog("NewFoodPickerViewController saveButtone image \(imageIdentifier as Any)")
+ let pick = FoodPick(item: item, ratio: ratio, date: Date(), imageIdentifier: imageIdentifier)
+ foodPick = pick
+ foodManager?.record(pick)
+ previewImage = nil
+ previewFileName = nil
+ AnalyticsManager.shared.didAddCarbsFromFoodPicker(pick)
+ self.performSegue(withIdentifier: "close", sender: nil)
+ }
+ }
+
+
+ @IBAction func unwindToFoodPicker(sender: UIStoryboardSegue)
+ {
+ guard let source = sender.source as? FoodPickerCameraViewController else {
+ return
+ }
+ NSLog("unwindToFoodPicker we are back!")
+
+ previewImage = source.imageOutput
+ selected = source.selectedPath
+
+ if let image = previewImage, let foodManager = foodManager {
+ previewFileName = foodManager.saveCustomImage(image)
+ }
+
+ NSLog("unwindToFoodPicker filename \(String(describing: previewFileName)) selected \(String(describing: selected))")
+
+ if let indexPath = selected {
+ saveButtonItem.isEnabled = true
+
+ updateTopControls(indexPath: indexPath)
+ collectionView.reloadItems(at: [indexPath])
+
+ }
+
+ }
+
+
+ func foodItemForPath(_ indexPath : IndexPath) -> FoodItem? {
+ if let foodManager = foodManager {
+ let sectionName = foodManager.sections[indexPath.section]
+ let category = foodManager.categories[sectionName]
+ return category?[indexPath.item]
+ //return foodManager.items[indexPath.item]
+ }
+ return nil
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ foodPick = nil
+ /*
+ previewImage = nil
+ previewFileName = nil
+ */
+ collectionView.dataSource = self
+ collectionView.delegate = self
+ collectionView.collectionViewLayout = FoodPickerFlowLayout()
+
+ updateTopControls(indexPath: selected)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ AnalyticsManager.shared.didDisplayFoodPicker()
+ }
+
+
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+
+ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "foodImageCell" , for: indexPath) as! FoodCollectionViewCell
+
+
+ if let item = foodItemForPath(indexPath) {
+ cell.imageView.layer.masksToBounds = true
+ cell.imageView.clipsToBounds = true
+ if item.title == "Photo", previewImage != nil {
+ cell.imageView.image = previewImage
+ } else {
+ cell.imageView.image = foodManager?.image(item: item)
+ }
+ if cell.imageView.image == nil {
+ let carbs = Int(round(item.carbPortion))
+ cell.carbLabel?.text = "\(carbs)"
+ } else {
+ cell.carbLabel?.text = ""
+ }
+
+ cell.foodLabel?.text = "\(item.title)"
+
+ if self.selected == indexPath {
+ cell.backgroundColor = UIColor(red: 0.278, green: 0.694, blue: 0.537, alpha: 1.00)
+
+ // cell.contentView.backgroundColor = UIColor(colorLiteralRed: 0.278, green: 0.694, blue: 0.537, alpha: 1.00)
+ } else {
+ cell.backgroundColor = UIColor.white
+
+ }
+ }
+ return cell
+ }
+
+ func numberOfSections(in collectionView: UICollectionView) -> Int {
+ guard let foodManager = foodManager else { return 0; }
+ return foodManager.sections.count
+ }
+
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ guard let foodManager = foodManager else { return 0; }
+ let sectionName = foodManager.sections[section]
+ let category = foodManager.categories[sectionName]
+ return category!.count
+ }
+
+ func updateTopControls(indexPath: IndexPath?) {
+ guard let indexPath = indexPath else {
+ sliderMultiplier.isEnabled = false
+ sliderMultiplier.isHidden = true
+ selectionMultiplier.isHidden = true
+ foodInfo.text = "Select food!"
+ sliderInfo.text = ""
+ saveButtonItem.isEnabled = false
+ return
+ }
+ if let foodManager = self.foodManager, let item = foodItemForPath(indexPath) {
+
+ let meta = foodManager.metaData(item)
+ saveButtonItem.isEnabled = true
+
+ if let slider = sliderMultiplier, let selector = selectionMultiplier {
+ slider.isEnabled = true
+ switch meta.type {
+ case .continuous:
+ slider.minimumValue = Float(item.portionSize / 4)
+ slider.maximumValue = Float(item.portionSize * 2)
+ slider.setValue(Float(item.portionSize), animated: false)
+ slider.isHidden = false
+ selector.isHidden = true
+ case .drink:
+ slider.minimumValue = Float(item.portionSize / 4)
+ slider.maximumValue = max(Float(item.portionSize * 2), 500)
+ slider.setValue(Float(item.portionSize), animated: false)
+ slider.isHidden = false
+ selector.isHidden = true
+ case .multiple:
+ slider.minimumValue = 1
+ slider.maximumValue = 8
+ slider.setValue(Float(meta.initial), animated: false)
+ slider.isHidden = false
+ selector.isHidden = true
+
+ case .single:
+ selector.selectedSegmentIndex = 2
+ slider.isHidden = true
+ selector.isHidden = false
+
+ }
+ sliderValueChanged(slider)
+ }
+ }
+ }
+
+ override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+ super.prepare(for: segue, sender: sender)
+
+ var targetViewController = segue.destination
+ if let navVC = targetViewController as? UINavigationController, let topViewController = navVC.topViewController {
+ targetViewController = topViewController
+ }
+ switch targetViewController {
+ case let vc as FoodPickerCameraViewController:
+ vc.selectedPath = selected
+ default:
+ break
+ }
+ }
+
+ func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+ var reloadPath = [indexPath]
+ if let selected = self.selected {
+ reloadPath.append(selected)
+ }
+ self.selected = indexPath
+ if let item = foodItemForPath(indexPath) {
+ if item.title == "Photo" {
+ performSegue(withIdentifier: FoodPickerCameraViewController.className, sender: self)
+ return
+ }
+ }
+ updateTopControls(indexPath: indexPath)
+
+ collectionView.reloadItems(at: reloadPath)
+ }
+
+ @IBAction func selectionValueChanged(_ sender: Any) {
+ sliderValueChanged(self.sliderMultiplier)
+ }
+
+
+ @IBAction func sliderMultiplierValueChanged(_ sender: Any) {
+ sliderValueChanged(self.sliderMultiplier)
+ }
+
+ func sliderValueChanged(_ sender : UISlider) {
+ guard let foodManager = self.foodManager else { return }
+ guard let selected = self.selected else { return }
+ guard let item = foodItemForPath(selected) else { return }
+
+ let meta = foodManager.metaData(item)
+
+ let selector = selectionMultiplier
+
+ var value = sender.value
+ var total = 0.0
+ var unit = "g"
+ switch meta.type {
+
+ case .single:
+ switch(selector?.selectedSegmentIndex ?? 2) {
+ case 0: value = 0.5
+ case 1: value = 0.7
+ case 2: value = 1
+ case 3: value = 1.5
+ default: value = 1
+ }
+
+ sliderInfo.text = ""
+ total = item.portionSize * Double(value)
+ ratio = Double(value)
+
+ case .multiple:
+ value = round(value)
+ let intvalue = Int(value)
+ sliderInfo.text = "\(intvalue)x"
+ total = item.portionSize * Double(value)
+ ratio = Double(value)
+
+ case .continuous:
+ value = round(value / 10) * 10
+ let intvalue = Int(value)
+ sliderInfo.text = "\(intvalue)g"
+ total = Double(value)
+ ratio = total / item.portionSize
+
+ case .drink:
+ value = round(value / 50) * 50
+ let intvalue = Int(value)
+ sliderInfo.text = "\(intvalue)ml"
+ total = Double(value)
+ ratio = total / item.portionSize
+ unit = "ml"
+ }
+ sender.setValue(value, animated: false)
+
+ let cr = Int(round(item.carbRatio * 100))
+
+ let ps = Int(round(total))
+ let cp = Int(round(total * item.carbRatio))
+
+ let title = meta.subtitle ?? item.title
+ foodInfo.text = "\(title): \(ps) \(unit), Carbs \(cp) g (\(cr)%)"
+ }
+
+
+ func collectionView(_ collectionView: UICollectionView,
+ viewForSupplementaryElementOfKind kind: String,
+ at indexPath: IndexPath) -> UICollectionReusableView {
+ switch kind {
+ case UICollectionElementKindSectionHeader:
+ //3
+ let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FoodCollectionReusableView", for: indexPath) as! FoodCollectionReusableView
+ let sectionName = foodManager?.sections[indexPath.section]
+ view.headerLabel.text = sectionName ?? "Undefined"
+ return view
+ default:
+ assert(false, "Unexpected element kind")
+ }
+ return UICollectionReusableView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
+ }
+
+
+}
diff --git a/Loop/View Controllers/NoteTableViewController.swift b/Loop/View Controllers/NoteTableViewController.swift
new file mode 100644
index 0000000000..5acf07c521
--- /dev/null
+++ b/Loop/View Controllers/NoteTableViewController.swift
@@ -0,0 +1,44 @@
+//
+// NoteTableViewController.swift
+// Loop
+//
+// Copyright © 2016 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import UIKit
+import CarbKit
+import HealthKit
+
+
+final class NoteTableViewController: UITableViewController, IdentifiableClass {
+
+ @IBOutlet weak var noteBox: UITextView!
+
+ var saved = false
+ var text = ""
+
+ @IBAction func saveButton(_ sender: Any) {
+ self.saved = true
+ self.text = noteBox.text!
+ self.performSegue(withIdentifier: "close", sender: nil)
+ }
+
+ override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+ super.prepare(for: segue, sender: sender)
+
+ self.noteBox.becomeFirstResponder()
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ self.noteBox.becomeFirstResponder()
+ }
+
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+ textField.resignFirstResponder()
+
+ return true
+ }
+}
diff --git a/Loop/View Controllers/QuickCarbEntryViewController.swift b/Loop/View Controllers/QuickCarbEntryViewController.swift
new file mode 100644
index 0000000000..9cf186aca2
--- /dev/null
+++ b/Loop/View Controllers/QuickCarbEntryViewController.swift
@@ -0,0 +1,240 @@
+//
+// QuickCarbEntryViewController.swift
+// Loop
+//
+// Copyright © 2016 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import UIKit
+import CarbKit
+import HealthKit
+
+
+final class QuickCarbEntryViewController: UITableViewController, IdentifiableClass {
+ var defaultAbsorptionTimes : CarbStore.DefaultAbsorptionTimes? = nil
+ private var internalCarbs : Int = 0
+ @IBOutlet weak var glucoseCell: UITableViewCell!
+
+ @IBOutlet weak var carbOnBoardLabel: UILabel!
+ @IBOutlet weak var carbEntryLabel: UILabel!
+
+ @IBOutlet weak var absorptionCell: UITableViewCell!
+ @IBOutlet weak var noteTextField: UITextField!
+
+ @IBOutlet weak var saveButton: UIButton!
+
+ var carbStore: CarbStore?
+
+ // private var absorptionTime : Int = 180
+ var carbs : CarbEntry? = nil
+ var glucose : HKQuantity? = nil
+ private var internalGlucose : Int = 0
+ let foodType = "quickCarbs"
+ var carbWarning : Double = 100.0
+
+ var carbsOnBoard : Double = 0.0
+ var lastCarbEntry : CarbEntry? = nil
+ var totalMealCarbs : Int? = nil
+ var mealStart : Date? = nil
+
+ var shouldShowAbsorption : Bool = false // Allow to chose aborption times
+
+ var shouldShowGlucose : Bool = false
+ var initialGlucose : Int? = nil
+ private var glucoseIsModified : Bool = false
+
+ var saved : Bool = false
+
+ var automatedBolusEnabled : Bool = false
+
+ var noteEntered : String = ""
+
+ public var preferredGlucoseUnit: HKUnit = HKUnit.milligramsPerDeciliter()
+ public var preferredUnit: HKUnit = HKUnit.gram()
+
+ override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+ var rval:CGFloat = 30
+ switch indexPath.row {
+ case 0: rval = 80 // info
+ case 1: rval = 100 // entry
+ case 2: if self.shouldShowAbsorption { rval = 60 } else { rval = 0 } // absorption
+ case 3: if self.shouldShowGlucose { rval = 60 } else { rval = 0 } // glucose
+ case 4: rval = 44 // save
+
+ default: rval = 30
+ }
+ return rval
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ self.carbs = nil
+ self.glucose = nil
+ self.saved = false
+
+ self.internalCarbs = 0
+
+ self.internalGlucose = 0
+ self.glucoseIsModified = false
+ if self.initialGlucose != nil {
+ self.internalGlucose = self.initialGlucose!
+ glucoseTextField.text = "\(self.internalGlucose)"
+ } else {
+ glucoseTextField.text = ""
+ }
+ if self.carbsOnBoard >= 0 {
+ let value = Int(self.carbsOnBoard)
+ self.carbOnBoardLabel.text = "Carbs on board: \(value) g"
+ } else {
+ self.carbOnBoardLabel.text = "Carbs on board: unknown"
+ }
+ /*
+ if let carb = self.lastCarbEntry {
+ let amount = Int(carb.quantity.doubleValue(for: preferredUnit))
+ let formatter = DateFormatter()
+ formatter.dateStyle = .none
+ formatter.timeStyle = .short
+ let dateStr = formatter.string(from: carb.startDate)
+ self.carbEntryLabel.text = "Last carb entry: \(amount) g @\(dateStr)"
+ } else {
+ self.carbEntryLabel.text = "No carbs in the last 3 hours."
+ }
+ */
+ if let mealStart = mealStart, let carbs = self.totalMealCarbs {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .none
+ formatter.timeStyle = .short
+ let dateStr = formatter.string(from: mealStart)
+ self.carbEntryLabel.text = "Meal carbs: \(carbs) g since \(dateStr)"
+ self.carbOnBoardLabel.text = "! CAREFUL TO NOT ENTER CARBS AGAIN IF BOLUS FAILED !"
+ self.carbOnBoardLabel.textColor = UIColor.red
+ self.internalCarbs = 0
+ } else {
+ if self.carbsOnBoard >= 0 {
+ let value = Int(self.carbsOnBoard)
+ self.carbOnBoardLabel.text = "Carbs on board: \(value) g"
+ } else {
+ self.carbOnBoardLabel.text = "Carbs on board: unknown"
+ }
+ self.carbEntryLabel.text = "No meal in progress."
+ }
+
+ if !self.shouldShowGlucose {
+ glucoseCell.isHidden = true
+ // todo set height
+ }
+ updateCarbLabel()
+
+ if self.automatedBolusEnabled {
+ self.saveButton.setTitle("Save and automatic Bolus", for: .normal)
+ } else {
+ self.saveButton.setTitle("Save and go to Bolus", for: .normal)
+
+ }
+ }
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ AnalyticsManager.shared.didDisplayQuickCarbScreen()
+ }
+
+ @IBOutlet weak var glucoseTextField: UITextField!
+
+ @IBAction func decrementGlucoseButton(_ sender: Any) {
+ if self.internalGlucose == 0 {
+ self.internalGlucose = 100
+ } else {
+ self.internalGlucose = max(0, self.internalGlucose - 5)
+ }
+ self.glucoseIsModified = true
+ updateGlucoseLabel()
+ }
+
+ @IBAction func incrementGlucoseButton(_ sender: Any) {
+ if self.internalGlucose == 0 {
+ self.internalGlucose = 100
+ } else {
+ self.internalGlucose = max(0, self.internalGlucose + 5)
+ updateGlucoseLabel()
+ }
+ self.glucoseIsModified = true
+ }
+ private func updateGlucoseLabel() {
+ glucoseTextField.text = "\(self.internalGlucose)"
+
+ }
+
+ @IBOutlet weak var absorptionTimeControl: UISegmentedControl!
+
+ private func updateCarbLabel() {
+ carbLabel.text = "\(self.internalCarbs) g"
+ }
+ @IBOutlet weak var carbLabel: UILabel!
+
+ @IBAction func decrementButton(_ sender: Any) {
+ self.internalCarbs = max(0, self.internalCarbs - 5)
+ updateCarbLabel()
+ }
+
+ @IBAction func incrementButton(_ sender: Any) {
+ self.internalCarbs = self.internalCarbs + 5
+ updateCarbLabel()
+ }
+
+ @IBAction func saveButton(_ sender: Any) {
+ if let mealCarbs = totalMealCarbs, internalCarbs > 0 {
+ let futureCarbs = mealCarbs + internalCarbs
+
+ guard futureCarbs <= Int(carbWarning) else {
+ let alert = UIAlertController(title: "Exceeds Usual Carbs", message: "The carb amount of \(internalCarbs) g together with previously entered carbs of \(mealCarbs) g is higher than the usual amount of \(carbWarning) g. If you are sure that this meal is in total \(futureCarbs) re-enter the amount of \(internalCarbs) g to confirm.", preferredStyle: .alert)
+
+ alert.addTextField { (textField) in
+ textField.text = ""
+ textField.keyboardType = UIKeyboardType.decimalPad
+ textField.autocorrectionType = UITextAutocorrectionType.no
+ }
+
+ alert.addAction(UIAlertAction(title: "Confirm Carbs", style: .default, handler: { [weak alert] (_) in
+ let wanted = "\(self.internalCarbs)"
+ let result = alert?.textFields![0].text
+ if result != nil && result! == wanted {
+ self.setCarbsAndClose()
+ } else {
+ self.presentAlertController(withTitle: NSLocalizedString("Exceeds Usual Carbs", comment: "The title of the alert describing a carbs validation error"), message: String(format: NSLocalizedString("The Validation failed (wanted \(wanted), entered \(result!))", comment: "Body of the alert describing a carb validation error. (1: The localized carb value)")))
+ return
+ }
+ }))
+
+ alert.addAction(UIAlertAction(title: "Back", style: .default, handler: nil))
+
+ self.present(alert, animated: true, completion: nil)
+
+ return
+ }
+ }
+
+ self.setCarbsAndClose()
+ }
+
+ func setCarbsAndClose() {
+
+ if self.internalCarbs > 0 {
+ let quantity = HKQuantity(unit: HKUnit.gram(), doubleValue: Double(self.internalCarbs))
+ let absorptionTime = AbsorptionSpeed.normal.seconds
+ self.carbs = NewCarbEntry(quantity: quantity, startDate: Date(), foodType: self.foodType,
+ absorptionTime: absorptionTime)
+ }
+
+ if self.internalGlucose > 0 && glucoseIsModified {
+ self.glucose = HKQuantity(unit: preferredGlucoseUnit, doubleValue: Double(self.internalGlucose))
+ }
+
+ if let text = self.noteTextField.text {
+ self.noteEntered = text
+ }
+ self.saved = true
+ AnalyticsManager.shared.didAddCarbsFromQuickCarbs(self.internalCarbs, self.internalGlucose, self.noteEntered)
+ self.performSegue(withIdentifier: "close", sender: nil)
+ }
+
+}
diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift
index 9965adefdb..8531b88294 100644
--- a/Loop/View Controllers/SettingsTableViewController.swift
+++ b/Loop/View Controllers/SettingsTableViewController.swift
@@ -65,6 +65,10 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
dataManager.rileyLinkManager.setScanningEnabled(false)
rssiFetchTimer = nil
+
+ if let uploader = dataManager.remoteDataManager.nightscoutService.uploader {
+ UserDefaults.appGroup.uploadProfile(uploader: uploader)
+ }
}
var dataManager: DeviceDataManager!
@@ -80,6 +84,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
fileprivate enum LoopRow: Int, CaseCountable {
case dosing = 0
+ case bolus
case preferredInsulinDataSource
case diagnostic
}
@@ -101,11 +106,14 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
case glucoseTargetRange = 0
case suspendThreshold
case insulinModel
+ case minBasalRate
case basalRate
+ case basalRateSetPump
case carbRatio
case insulinSensitivity
case maxBasal
case maxBolus
+ case maxInsulinOnBoard
}
fileprivate enum ServiceRow: Int, CaseCountable {
@@ -125,6 +133,9 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
return formatter
}()
+ fileprivate var setBasalRateLabel : UILabel?
+ fileprivate let basalRateSem = DispatchSemaphore(value: 1)
+
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.destination {
case let vc as InsulinModelSettingsViewController:
@@ -190,6 +201,15 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
switchCell.switch?.addTarget(self, action: #selector(dosingEnabledChanged(_:)), for: .valueChanged)
+ return switchCell
+ case .bolus:
+ let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell
+
+ switchCell.switch?.isOn = dataManager.loopManager.settings.bolusEnabled
+ switchCell.textLabel?.text = NSLocalizedString("Automated Bolus", comment: "The title text for the automated bolus enabled switch cell")
+
+ switchCell.switch?.addTarget(self, action: #selector(bolusEnabledChanged(_:)), for: .valueChanged)
+
return switchCell
case .preferredInsulinDataSource:
let cell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath)
@@ -277,6 +297,19 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
} else {
configCell.detailTextLabel?.text = TapToSetString
}
+ case .basalRateSetPump:
+ configCell.textLabel?.text = NSLocalizedString("Write Basal Rates", comment: "The title text for writing the basal rate schedule to the pump")
+
+ configCell.detailTextLabel?.text = "Tap to write"
+ setBasalRateLabel = configCell.detailTextLabel
+ case .minBasalRate:
+ configCell.textLabel?.text = NSLocalizedString("Minimum Basal Rates", comment: "The title text for the minimum basal rate schedule")
+
+ if let minimumBasalRateSchedule = dataManager.loopManager.minimumBasalRateSchedule {
+ configCell.detailTextLabel?.text = "\(minimumBasalRateSchedule.total()) U"
+ } else {
+ configCell.detailTextLabel?.text = TapToSetString
+ }
case .carbRatio:
configCell.textLabel?.text = NSLocalizedString("Carb Ratios", comment: "The title text for the carb ratio schedule")
@@ -345,6 +378,14 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
} else {
configCell.detailTextLabel?.text = TapToSetString
}
+ case .maxInsulinOnBoard:
+ configCell.textLabel?.text = NSLocalizedString("Maximum IOB", comment: "The title text for the maximum insulin on board value")
+
+ if let maxInsulinOnBoard = dataManager.loopManager.settings.maximumInsulinOnBoard {
+ configCell.detailTextLabel?.text = "\(valueNumberFormatter.string(from: NSNumber(value: maxInsulinOnBoard))!) U"
+ } else {
+ configCell.detailTextLabel?.text = TapToSetString
+ }
}
return configCell
@@ -484,7 +525,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
case .configuration:
let row = ConfigurationRow(rawValue: indexPath.row)!
switch row {
- case .maxBasal, .maxBolus:
+ case .maxBasal, .maxBolus, .maxInsulinOnBoard:
let vc: LoopKit.TextFieldTableViewController
switch row {
@@ -492,6 +533,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
vc = .maxBasal(dataManager.loopManager.settings.maximumBasalRatePerHour)
case .maxBolus:
vc = .maxBolus(dataManager.loopManager.settings.maximumBolus)
+ case .maxInsulinOnBoard:
+ vc = .maxInsulinOnBoard(dataManager.loopManager.settings.maximumInsulinOnBoard)
default:
fatalError()
}
@@ -511,6 +554,38 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
scheduleVC.delegate = self
scheduleVC.title = NSLocalizedString("Basal Rates", comment: "The title of the basal rate profile screen")
+ show(scheduleVC, sender: sender)
+ case .basalRateSetPump:
+ NSLog("Calling setBasalRates")
+ if basalRateSem.wait(timeout: .now()) == .success {
+ setBasalRateLabel?.text = "Writing"
+ dataManager.setBasalRate { (error) in
+ DispatchQueue.main.async {
+ if let error = error {
+ NSLog("settings setBasalRates error \(error)")
+ self.presentAlertController(with: error)
+ self.setBasalRateLabel?.text = "Error \(error)"
+ self.basalRateSem.signal()
+ } else {
+ NSLog("settings setBasalRates success")
+ self.setBasalRateLabel?.text = "Success"
+ self.basalRateSem.signal()
+ }
+ }
+ }
+ } else {
+ NSLog(" setBasalRates already in progress")
+ }
+ case .minBasalRate:
+ let scheduleVC = SingleValueScheduleTableViewController()
+
+ if let profile = dataManager.loopManager.minimumBasalRateSchedule {
+ scheduleVC.timeZone = profile.timeZone
+ scheduleVC.scheduleItems = profile.items
+ }
+ scheduleVC.delegate = self
+ scheduleVC.title = NSLocalizedString("Minimum Basal Rates", comment: "The title of the minimum basal rate profile screen")
+
show(scheduleVC, sender: sender)
case .carbRatio:
let scheduleVC = DailyQuantityScheduleTableViewController()
@@ -599,6 +674,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
}
case .insulinModel:
performSegue(withIdentifier: InsulinModelSettingsViewController.className, sender: sender)
+
+
}
case .devices:
let device = devices[indexPath.row]
@@ -629,7 +706,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
vc.title = sender?.textLabel?.text
show(vc, sender: sender)
- case .dosing:
+ case .dosing, .bolus:
break
}
case .services:
@@ -692,6 +769,10 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
@objc private func dosingEnabledChanged(_ sender: UISwitch) {
dataManager.loopManager.settings.dosingEnabled = sender.isOn
}
+
+ @objc private func bolusEnabledChanged(_ sender: UISwitch) {
+ dataManager.loopManager.settings.bolusEnabled = sender.isOn
+ }
@objc private func reloadDevices() {
self.dataManager.rileyLinkManager.getDevices { (devices) in
@@ -873,6 +954,11 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
dataManager.loopManager.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone)
AnalyticsManager.shared.didChangeBasalRateSchedule()
}
+ case .minBasalRate:
+ if let controller = controller as? SingleValueScheduleTableViewController {
+ dataManager.loopManager.minimumBasalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone)
+ AnalyticsManager.shared.didChangeBasalRateSchedule()
+ }
case .glucoseTargetRange:
if let controller = controller as? GlucoseRangeScheduleTableViewController {
dataManager.loopManager.settings.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, timeZone: controller.timeZone, overrideRanges: controller.overrideRanges, override: dataManager.loopManager.settings.glucoseTargetRangeSchedule?.override)
@@ -1012,6 +1098,12 @@ extension SettingsTableViewController: LoopKit.TextFieldTableViewControllerDeleg
} else {
dataManager.loopManager.settings.maximumBolus = nil
}
+ case .maxInsulinOnBoard:
+ if let value = controller.value, let units = valueNumberFormatter.number(from: value)?.doubleValue {
+ dataManager.loopManager.settings.maximumInsulinOnBoard = units
+ } else {
+ dataManager.loopManager.settings.maximumInsulinOnBoard = nil
+ }
default:
assertionFailure()
}
diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift
index 0d690ffcf0..0cdb9d702b 100644
--- a/Loop/View Controllers/StatusTableViewController.swift
+++ b/Loop/View Controllers/StatusTableViewController.swift
@@ -31,7 +31,7 @@ private extension RefreshContext {
}
-final class StatusTableViewController: ChartsTableViewController {
+final class StatusTableViewController: ChartsTableViewController, MealTableViewCellDelegate {
override func viewDidLoad() {
super.viewDidLoad()
@@ -88,10 +88,21 @@ final class StatusTableViewController: ChartsTableViewController {
// Toolbar
toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button")
toolbarItems![0].tintColor = UIColor.COBTintColor
+ // toolbarItems![0].action = #selector(showQuickCarbEntry(_:))
+
+ toolbarItems![2] = createNoteButtonItem()
+
toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button")
toolbarItems![4].tintColor = UIColor.doseTintColor
toolbarItems![8].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button")
toolbarItems![8].tintColor = UIColor.secondaryLabelColor
+
+ let longTapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(toggleExpertMode(_:)))
+ longTapGestureRecognizer.minimumPressDuration = 0.3
+
+ self.navigationController?.toolbar.addGestureRecognizer(longTapGestureRecognizer)
+
+ toolbarItems![8].isEnabled = expertMode
}
override func didReceiveMemoryWarning() {
@@ -179,7 +190,59 @@ final class StatusTableViewController: ChartsTableViewController {
private var shouldShowStatus: Bool {
return !landscapeMode && statusRowMode.hasRow
}
-
+
+ private func updateMealInformation(_ updateGroup: DispatchGroup, _ carbStore: CarbStore?) {
+ //dispatchPrecondition(condition: .onQueue(dataAccessQueue))
+ if let carbStore = carbStore {
+ let endDate = Date()
+ let mealDate = endDate.addingTimeInterval(TimeInterval(minutes: -45))
+
+ let undoPossibleDate = endDate.addingTimeInterval(TimeInterval(minutes: -15))
+ updateGroup.enter()
+ //carbStore.getCarbEntries(start: mealDate) { (result) in
+ // switch(result) {
+ // case .success(let values):
+ carbStore.getCachedCarbEntries(start: mealDate) { (values) in
+
+ var mealStart = endDate
+ var mealEnd = mealDate
+ var carbs : Double = 0
+ var allPicks = FoodPicks()
+ for value in values {
+ // NSLog(" updateMealInformation - v - \(value)")
+ mealStart = min(mealStart, value.startDate)
+ mealEnd = max(value.startDate, mealStart)
+ let picks = value.foodPicks()
+ for pick in picks.picks {
+ allPicks.append(pick)
+ }
+ // NSLog(" updateMealInformation - p - \(picks)")
+ if let lastpick = picks.last {
+ mealEnd = max(lastpick.date, mealEnd)
+ }
+ carbs = carbs + picks.carbs
+
+ }
+ // NSLog(" updateMealInformation - carbs - \(carbs)")
+ //var undoPossible = false
+ //if let undoPossibleDate = undoPossibleDate {
+ let undoPossible = undoPossibleDate <= mealEnd
+ //}
+ self.mealInformation = (date: endDate, lastCarbEntry: values.last,
+ picks: allPicks,
+ start: mealStart, end: mealEnd, carbs: carbs, undoPossible: undoPossible)
+
+ NSLog("updateMealInformation - \(self.mealInformation as Any)")
+ //case .failure(let error):
+ // NSLog("updateMealInformation - error - ", error as Any)
+ //}
+ updateGroup.leave()
+ }
+ }
+ //mealInformationNeedsUpdate = false
+ }
+
+
override func reloadData(animated: Bool = false) {
guard active && visible && !reloading && !refreshContext.isEmpty && !deviceManager.loopManager.authorizationRequired else {
return
@@ -194,7 +257,7 @@ final class StatusTableViewController: ChartsTableViewController {
let availableWidth = (currentContext.newSize ?? self.tableView.bounds.size).width - self.charts.fixedHorizontalMargin
let totalHours = floor(Double(availableWidth / minimumSegmentWidth))
let futureHours = ceil((deviceManager.loopManager.insulinModelSettings?.model.effectDuration ?? .hours(4)).hours)
- let historyHours = max(1, totalHours - futureHours)
+ let historyHours = max(2.5, totalHours - futureHours)
var components = DateComponents()
components.minute = 0
@@ -220,6 +283,7 @@ final class StatusTableViewController: ChartsTableViewController {
// TODO: Don't always assume currentContext.contains(.status)
reloadGroup.enter()
+ StatisticsManager.shared.inc("reloadData")
self.deviceManager.loopManager.getLoopState { (manager, state) -> Void in
self.charts.setPredictedGlucoseValues(state.predictedGlucose ?? [])
@@ -233,7 +297,21 @@ final class StatusTableViewController: ChartsTableViewController {
let lastLoopCompleted = state.lastLoopCompleted
let lastLoopError = state.error
let dosingEnabled = manager.settings.dosingEnabled
-
+
+ //self.mealInformation = state.mealInformation
+
+
+ self.pumpDetachedMode = state.pumpDetachedMode
+ self.treatmentInformation = state.treatmentInformation
+
+
+ self.validGlucose = state.validGlucose
+ if let _ = self.validGlucose {
+ self.needManualGlucose = nil
+ } else {
+ self.needManualGlucose = Date()
+ }
+
// Net basal rate HUD
let date = state.lastTempBasal?.startDate ?? Date()
if let scheduledBasal = manager.basalRateSchedule?.between(start: date, end: date).first {
@@ -245,6 +323,10 @@ final class StatusTableViewController: ChartsTableViewController {
} else {
netBasal = nil
}
+
+ // if state.lastRequestedBolus != nil {
+ // self.bolusState = .enacting
+ // }
DispatchQueue.main.async {
self.hudView?.loopCompletionHUD.lastLoopCompleted = lastLoopCompleted
@@ -254,6 +336,11 @@ final class StatusTableViewController: ChartsTableViewController {
if let netBasal = netBasal {
self.hudView?.basalRateHUD.setNetBasalRate(netBasal.rate, percent: netBasal.percent, at: netBasal.start)
}
+ if let ti = self.treatmentInformation {
+ self.toolbarItems![4].isEnabled = ti.allowed
+ } else {
+ self.toolbarItems![4].isEnabled = false // fail closed
+ }
}
// Display a recommended basal change only if we haven't completed recently, or we're in open-loop mode
@@ -285,6 +372,8 @@ final class StatusTableViewController: ChartsTableViewController {
reloadGroup.leave()
}
}
+
+ self.updateMealInformation(reloadGroup, manager.carbStore)
reloadGroup.leave()
}
@@ -412,8 +501,12 @@ final class StatusTableViewController: ChartsTableViewController {
statusRowMode = nil
case .none:
if let (recommendation: tempBasal, date: date) = newRecommendedTempBasal {
- // TODO: Don't display this if we're in closed mode and the loop recently completed
- statusRowMode = .recommendedTempBasal(tempBasal: tempBasal, at: date, enacting: false)
+ // TODO(Erik) DDisplay this if we're in closed mode and the loop recently failed
+ if self.deviceManager.loopManager.settings.dosingEnabled {
+ statusRowMode = .hidden
+ } else {
+ statusRowMode = .recommendedTempBasal(tempBasal: tempBasal, at: date, enacting: false)
+ }
} else {
statusRowMode = .hidden
}
@@ -443,10 +536,14 @@ final class StatusTableViewController: ChartsTableViewController {
private enum Section: Int {
case hud = 0
- case status
+ case status // Bolus or TempBasalRecommendation
+ case detached // DetachedMode reminder
+ case treatment // Advanced Treatment and ongoing Bolus Information
+ case glucose // Glucose not available reminder
+ case meal // Meal Information
case charts
- static let count = 3
+ static let count = 7
}
// MARK: - Chart Section Data
@@ -493,6 +590,9 @@ final class StatusTableViewController: ChartsTableViewController {
switch self {
case .hidden:
return false
+ case .enactingBolus:
+ // Managed by different row in this version.
+ return false
default:
return true
}
@@ -500,15 +600,56 @@ final class StatusTableViewController: ChartsTableViewController {
}
private var statusRowMode = StatusRowMode.hidden
+
+ private var shouldShowPumpDetached : Bool {
+ return !landscapeMode && (displayPumpDetachedMode != nil)
+ }
+ private var shouldShowTreatmentInformation: Bool {
+ // TODO(Erik) Take dismissed information into account (but needs to be updated by the HUD function below
+ if landscapeMode {
+ return false
+ }
+ if let ti = displayTreatmentInformation {
+ return ti.state != .none
+ }
+ return false
+ }
+
+ private var shouldShowNeedManualGlucose: Bool {
+ return !landscapeMode && (displayNeedManualGlucose != nil)
+ }
+
+ private var shouldShowMeal: Bool {
+ return !landscapeMode
+ }
+
private func updateHUDandStatusRows(statusRowMode: StatusRowMode?, newSize: CGSize?, animated: Bool) {
let hudWasVisible = self.shouldShowHUD
let statusWasVisible = self.shouldShowStatus
+ let treatmentWasVisible = self.shouldShowTreatmentInformation
+ let glucoseWasVisible = self.shouldShowNeedManualGlucose
+ let detachedWasVisible = self.shouldShowPumpDetached
+ let mealWasVisible = self.shouldShowMeal
let oldStatusRowMode = self.statusRowMode
if let statusRowMode = statusRowMode {
self.statusRowMode = statusRowMode
}
+
+
+ let oldTreatmentInformation = self.displayTreatmentInformation
+ let newTreatmentInformation = self.treatmentInformation
+ self.displayTreatmentInformation = newTreatmentInformation
+
+ let oldNeedManualGlucose = self.displayNeedManualGlucose
+ let newNeedManualGlucose = self.needManualGlucose
+ self.displayNeedManualGlucose = newNeedManualGlucose
+
+ let oldPumpDetachedMode = self.displayPumpDetachedMode
+ let newPumpDetachedMode = self.pumpDetachedMode
+ self.displayPumpDetachedMode = newPumpDetachedMode
+
if let newSize = newSize {
self.landscapeMode = newSize.width > newSize.height
@@ -516,7 +657,11 @@ final class StatusTableViewController: ChartsTableViewController {
let hudIsVisible = self.shouldShowHUD
let statusIsVisible = self.shouldShowStatus
-
+ let treatmentIsVisible = self.shouldShowTreatmentInformation
+ let glucoseIsVisible = self.shouldShowNeedManualGlucose
+ let detachedIsVisible = self.shouldShowPumpDetached
+ let mealIsVisible = self.shouldShowMeal
+
tableView.beginUpdates()
switch (hudWasVisible, hudIsVisible) {
@@ -566,6 +711,69 @@ final class StatusTableViewController: ChartsTableViewController {
break
}
+ let treatmentIndexPath = IndexPath(row: 0, section: Section.treatment.rawValue)
+ switch (treatmentWasVisible, treatmentIsVisible) {
+ case (true, true):
+ if let old = oldTreatmentInformation, let new = newTreatmentInformation,
+ !old.equal(new) {
+ self.tableView.reloadRows(at: [treatmentIndexPath], with: animated ? .top : .none)
+ }
+ if oldTreatmentInformation == nil, newTreatmentInformation != nil {
+ self.tableView.reloadRows(at: [treatmentIndexPath], with: animated ? .top : .none)
+ }
+ if oldTreatmentInformation != nil, newTreatmentInformation == nil {
+ self.tableView.reloadRows(at: [treatmentIndexPath], with: animated ? .top : .none)
+ }
+ case (false, true):
+ self.tableView.insertRows(at: [treatmentIndexPath], with: animated ? .top : .none)
+ case (true, false):
+ self.tableView.deleteRows(at: [treatmentIndexPath], with: animated ? .top : .none)
+ default:
+ break
+ }
+
+ let detachedIndexPath = IndexPath(row: 0, section: Section.detached.rawValue)
+ switch (detachedWasVisible, detachedIsVisible) {
+ case (true, true):
+ if oldPumpDetachedMode != newPumpDetachedMode {
+ self.tableView.reloadRows(at: [detachedIndexPath], with: animated ? .top : .none)
+ }
+ case (false, true):
+ self.tableView.insertRows(at: [detachedIndexPath], with: animated ? .top : .none)
+ case (true, false):
+ self.tableView.deleteRows(at: [detachedIndexPath], with: animated ? .top : .none)
+ default:
+ break
+ }
+
+ let glucoseIndexPath = IndexPath(row: 0, section: Section.glucose.rawValue)
+ switch (glucoseWasVisible, glucoseIsVisible) {
+ case (true, true):
+ if oldNeedManualGlucose != newNeedManualGlucose {
+ self.tableView.reloadRows(at: [glucoseIndexPath], with: animated ? .top : .none)
+ }
+ case (false, true):
+ self.tableView.insertRows(at: [glucoseIndexPath], with: animated ? .top : .none)
+ case (true, false):
+ self.tableView.deleteRows(at: [glucoseIndexPath], with: animated ? .top : .none)
+ default:
+ break
+ }
+
+ let mealIndexPath = IndexPath(row: 0, section: Section.meal.rawValue)
+ switch (mealWasVisible, mealIsVisible) {
+ case (true, true):
+ // TODO(Erik) Make dependent on mealInformation changing.
+ self.tableView.reloadRows(at: [mealIndexPath], with: .none)
+ case (false, true):
+ self.tableView.insertRows(at: [mealIndexPath], with: animated ? .top : .none)
+ case (true, false):
+ self.tableView.deleteRows(at: [mealIndexPath], with: animated ? .top : .none)
+ default:
+ break
+ }
+
+
tableView.endUpdates()
}
@@ -577,11 +785,11 @@ final class StatusTableViewController: ChartsTableViewController {
return
}
- if let preMealMode = preMealMode {
- toolbarItems![2] = createPreMealButtonItem(selected: preMealMode)
- } else {
- toolbarItems![2].isEnabled = false
- }
+// if let preMealMode = preMealMode {
+// toolbarItems![2] = createPreMealButtonItem(selected: preMealMode)
+// } else {
+// toolbarItems![2].isEnabled = false
+// }
}
}
@@ -613,6 +821,14 @@ final class StatusTableViewController: ChartsTableViewController {
return ChartRow.count
case .status:
return shouldShowStatus ? StatusRow.count : 0
+ case .detached:
+ return shouldShowPumpDetached ? 1 : 0
+ case .treatment:
+ return shouldShowTreatmentInformation ? 1 : 0
+ case .meal:
+ return shouldShowMeal ? 1 : 0
+ case .glucose:
+ return shouldShowNeedManualGlucose ? 1 : 0
}
}
@@ -695,8 +911,171 @@ final class StatusTableViewController: ChartsTableViewController {
}
}
+ return cell
+
+ case .treatment:
+ let cell = tableView.dequeueReusableCell(withIdentifier: "BolusTableViewCell", for: indexPath) as! TitleSubtitleTableViewCell
+ if let pending = self.treatmentInformation {
+ let description = pending.description()
+ let kind = pending.kind()
+ cell.titleLabel?.text = "\(description) \(kind)"
+ if pending.carbs > 0 {
+ cell.subtitleLabel?.text = String(format: NSLocalizedString("%1$@ g @ %2$@", comment: "The format for current carbs and time. (1: localized unit number)(2: localized time)"), NumberFormatter.localizedString(from: NSNumber(value: pending.carbs), number: .decimal), timeFormatter.string(from: pending.date))
+ } else {
+ cell.subtitleLabel?.text = String(format: NSLocalizedString("%1$@ U @ %2$@", comment: "The format for current bolus and time. (1: localized unit number)(2: localized time)"), NumberFormatter.localizedString(from: NSNumber(value: pending.units), number: .decimal), timeFormatter.string(from: pending.date))
+ }
+ var color : UIColor = UIColor.black
+ let spinWheel = pending.inProgress()
+
+ var readPump = false
+ switch(pending.state) {
+ case .sent:
+ color = UIColor.orange
+ readPump = true
+ case .maybefailed:
+ color = UIColor.orange
+ readPump = true
+ case .failed:
+ color = UIColor.red
+ case .timeout:
+ color = UIColor.red
+ case .pending:
+ readPump = true
+ default: _ = true
+ }
+ if readPump {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
+ self.deviceManager.triggerPumpDataRead()
+ }
+ }
+ if cell.explanationLabel != nil {
+ cell.explanationLabel?.text = pending.explanation(bolusEnabled: deviceManager.loopManager.settings.bolusEnabled)
+ }
+ cell.titleLabel?.textColor = color
+ cell.subtitleLabel?.textColor = color
+
+ if spinWheel {
+ let indicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
+ indicatorView.startAnimating()
+ cell.accessoryView = indicatorView
+ } else {
+ cell.accessoryView = nil
+ }
+ } else {
+ cell.titleLabel?.text = "Bolus?"
+ cell.subtitleLabel?.text = "- nil -"
+ cell.accessoryView = nil
+
+ }
+ cell.selectionStyle = .default
+
+ return cell
+ case .meal:
+ let cell = tableView.dequeueReusableCell(withIdentifier: "MealTableViewCell", for: indexPath) as! MealTableViewCell
+ //let dataSource = FoodRecentCollectionViewDataSource()
+
+ var foodPicks = FoodPicks()
+
+ var undoPossible = false
+ if let mi = self.mealInformation, let mealEnd = mi.end, mealEnd.timeIntervalSinceNow > TimeInterval(minutes: -30) {
+ let intcarbs = Int(mi.carbs ?? 0)
+ cell.currentCarbLabel.text = "\(intcarbs) g"
+ if let fp = mi.picks {
+ foodPicks = fp
+ }
+// if let estimator = mi.estimator {
+// let td = timeFormatter.string(from: estimator.start)
+// let ti = Int(estimator.absorbed)
+// let tr = Int(estimator.rate)
+//
+// cell.debugLabelTop.text = "@\(td)"
+// cell.debugLabelBottom.text = "\(ti) g, \(tr) g/h"
+// } else {
+ cell.debugLabelTop.text = ""
+ cell.debugLabelBottom.text = ""
+
+// }
+ if let start = mi.start, let end = mi.end {
+ let t1 = timeFormatter.string(from: start)
+ let t2 = timeFormatter.string(from: end)
+ if start > end {
+ // meal not started, show nothing.
+ cell.currentCarbDate.text = "(tap to eat)"
+ } else if t1 == t2 {
+ cell.currentCarbDate.text = "\(t1)"
+ } else {
+ cell.currentCarbDate.text = "\(t1) - \(t2)"
+ }
+ } else {
+
+ cell.currentCarbDate.text = "(tap to eat)"
+
+ }
+ undoPossible = mi.undoPossible
+ } else {
+ cell.currentCarbLabel.text = ""
+ cell.currentCarbDate.text = ""
+ cell.debugLabelTop.text = ""
+ cell.debugLabelBottom.text = ""
+ cell.currentCarbLabel.text = "0 g"
+ cell.currentCarbDate.text = "(tap to eat)"
+ }
+
+ if undoPossible, mealInformation?.lastCarbEntry != nil {
+ cell.undoLabel.text = "Undo"
+ cell.undoLabel.backgroundColor = UIColor.orange
+ } else {
+ cell.undoLabel.text = ""
+ cell.undoLabel.backgroundColor = UIColor.white
+ /*
+ if picks.count == 0 {
+ cell.undoLabel.text = "Start\nMeal"
+ } else {
+ cell.undoLabel.text = "Add\nmore"
+ }
+ */
+ }
+ //cell.undoLabel.frame = cell.lastItemView.frame
+ cell.leftImageView.tintColor = UIColor.COBTintColor
+ cell.leftImageView.image = UIImage(named: "fork")?.withRenderingMode(.alwaysTemplate)
+ // cell.leftImageView.image?.renderingMode = .alwaysTemplate
+ //cell.leftButton.tintColor = UIColor.COBTintColor
+ //cell.leftButton.render
+ //cell.recentFoodCollectionView.collectionViewLayout = FoodRecentPickerFlowLayout()
+ cell.delegate = self
+ if cell.recentFoodCollectionView.dataSource == nil {
+ cell.recentFoodCollectionView.dataSource = foodRecentCollectionViewDataSource //as UICollectionViewDataSource
+ }
+ foodRecentCollectionViewDataSource.foodManager = foodManager
+ foodRecentCollectionViewDataSource.foodPicks = foodPicks
+ cell.recentFoodCollectionView.reloadData()
+ cell.recentFoodCollectionView.collectionViewLayout.invalidateLayout()
+ return cell
+ case .detached:
+ let cell = tableView.dequeueReusableCell(withIdentifier: "DisconnectTableViewCell", for: indexPath) as! TitleSubtitleTableViewCell
+ if let detached = self.pumpDetachedMode {
+ let color = UIColor.red
+ cell.subtitleLabel?.text = "until " + timeFormatter.string(from: detached)
+ cell.explanationLabel?.textColor = color
+ cell.titleLabel?.textColor = color
+ cell.subtitleLabel?.textColor = color
+ cell.accessoryView = nil
+ }
+ cell.selectionStyle = .default
+
+ return cell
+ case .glucose:
+ let cell = tableView.dequeueReusableCell(withIdentifier: "GlucoseTableViewCell", for: indexPath) as! TitleSubtitleTableViewCell
+ if let glucoseDate = displayNeedManualGlucose {
+ cell.subtitleLabel?.text = timeFormatter.string(from: glucoseDate)
+ }
+ cell.accessoryView = nil
+
+ cell.selectionStyle = .default
+
return cell
}
+
}
private func tableView(_ tableView: UITableView, updateSubtitleFor cell: ChartTableViewCell, at indexPath: IndexPath) {
@@ -734,6 +1113,14 @@ final class StatusTableViewController: ChartsTableViewController {
}
case .hud, .status:
break
+ case .detached:
+ break
+ case .treatment:
+ break
+ case .glucose:
+ break
+ case .meal:
+ break
}
}
@@ -762,19 +1149,37 @@ final class StatusTableViewController: ChartsTableViewController {
}
case .hud, .status:
return UITableViewAutomaticDimension
+ case .treatment:
+ return 70 // UITableViewAutomaticDimension
+ case .meal:
+ return 120 //UITableViewAutomaticDimension
+ /*
+ if let mi = self.mealInformation, let lastEntry = mi.lastCarbEntry, lastEntry.foodPicks().picks.count > 0 {
+
+ return 120
+ } else {
+ return 70
+ }
+ */
+ case .detached:
+ return 70 // UITableViewAutomaticDimension
+ case .glucose:
+ return 70 // UITableViewAutomaticDimension
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch Section(rawValue: indexPath.section)! {
case .charts:
- switch ChartRow(rawValue: indexPath.row)! {
- case .glucose:
- performSegue(withIdentifier: PredictionTableViewController.className, sender: indexPath)
- case .iob, .dose:
- performSegue(withIdentifier: InsulinDeliveryTableViewController.className, sender: indexPath)
- case .cob:
- performSegue(withIdentifier: CarbAbsorptionViewController.className, sender: indexPath)
+ if expertMode {
+ switch ChartRow(rawValue: indexPath.row)! {
+ case .glucose:
+ performSegue(withIdentifier: PredictionTableViewController.className, sender: indexPath)
+ case .iob, .dose:
+ performSegue(withIdentifier: InsulinDeliveryTableViewController.className, sender: indexPath)
+ case .cob:
+ performSegue(withIdentifier: CarbAbsorptionViewController.className, sender: indexPath)
+ }
}
case .status:
switch StatusRow(rawValue: indexPath.row)! {
@@ -801,6 +1206,64 @@ final class StatusTableViewController: ChartsTableViewController {
}
case .hud:
break
+ case .treatment:
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ // clear bolus if in last/failed state
+ if let pending = self.treatmentInformation {
+ switch(pending.state) {
+ case .recommended:
+ if pending.units > 0 {
+ if !deviceManager.loopManager.settings.bolusEnabled {
+ deviceManager.enactBolus(units: pending.units ) { (_) in
+ DispatchQueue.main.async {
+ self.bolusState = nil
+ }
+ }
+ // TODO(Erik) This needs to be queued properly
+ self.treatmentDisplayDismissed = true
+ self.treatmentInformation = nil
+ } else {
+ performSegue(withIdentifier: BolusViewController.className, sender: indexPath)
+ }
+ } else if pending.carbs > 0 {
+ performSegue(withIdentifier: QuickCarbEntryViewController.className, sender: indexPath)
+ //performSegue(withIdentifier: "CarbEntryEditViewController", sender: indexPath)
+ }
+ case .failed:
+ self.treatmentDisplayDismissed = true
+ self.treatmentInformation = nil
+ performSegue(withIdentifier: BolusViewController.className, sender: indexPath)
+ case .timeout:
+ self.treatmentDisplayDismissed = true
+ self.treatmentInformation = nil
+ performSegue(withIdentifier: BolusViewController.className, sender: indexPath)
+ case .success:
+ self.treatmentDisplayDismissed = true
+ self.treatmentInformation = nil
+ default:
+ _ = true
+ }
+ }
+
+ DispatchQueue.main.async {
+ //self.needsRefresh = true
+ self.reloadData()
+ }
+ break
+ case .meal:
+ tableView.deselectRow(at: indexPath, animated: true)
+ case .detached:
+ tableView.deselectRow(at: indexPath, animated: true)
+ deviceManager.loopManager.disablePumpDetachedMode()
+ // TODO this should use a callback
+ DispatchQueue.main.async {
+ //self.needsRefresh = true
+ self.reloadData()
+ }
+ case .glucose:
+ tableView.deselectRow(at: indexPath, animated: true)
+ performSegue(withIdentifier: "QuickCarbEntryViewController", sender: indexPath)
}
}
@@ -831,12 +1294,21 @@ final class StatusTableViewController: ChartsTableViewController {
case let vc as BolusViewController:
vc.configureWithLoopManager(self.deviceManager.loopManager,
recommendation: sender as? BolusRecommendation,
- glucoseUnit: self.charts.glucoseUnit
+ glucoseUnit: self.charts.glucoseUnit,
+ expertMode: self.expertMode
)
case let vc as PredictionTableViewController:
vc.deviceManager = deviceManager
case let vc as SettingsTableViewController:
vc.dataManager = deviceManager
+ case let vc as NewFoodPickerViewController:
+ foodManager?.updatePopular()
+ vc.foodManager = foodManager
+ case let vc as QuickCarbEntryViewController:
+ vc.carbStore = deviceManager.loopManager.carbStore
+ vc.preferredGlucoseUnit = self.charts.glucoseUnit
+ vc.shouldShowGlucose = self.validGlucose == nil
+ vc.automatedBolusEnabled = deviceManager.loopManager.settings.bolusEnabled
default:
break
}
@@ -849,14 +1321,25 @@ final class StatusTableViewController: ChartsTableViewController {
guard let carbVC = segue.source as? CarbEntryEditViewController, let updatedEntry = carbVC.updatedCarbEntry else {
return
}
-
+ updateCarbEntry(updatedEntry: updatedEntry)
+ }
+
+ func updateCarbEntry(updatedEntry: CarbEntry) {
deviceManager.loopManager.addCarbEntryAndRecommendBolus(updatedEntry) { (result) -> Void in
DispatchQueue.main.async {
switch result {
case .success(let recommendation):
if self.active && self.visible, let bolus = recommendation?.amount, bolus > 0 {
- self.bolusState = .recommended
- self.performSegue(withIdentifier: BolusViewController.className, sender: recommendation)
+ if self.bolusState == nil {
+ self.bolusState = .recommended
+ }
+ if !self.deviceManager.loopManager.settings.bolusEnabled {
+ // TOOD(Erik): With no glucose but pump information we should still propose
+ // a bolus based on the amount of carbs.
+ if let ti = self.treatmentInformation, ti.allowed {
+ self.performSegue(withIdentifier: BolusViewController.className, sender: recommendation)
+ }
+ }
}
case .failure(let error):
// Ignore bolus wizard errors
@@ -872,6 +1355,13 @@ final class StatusTableViewController: ChartsTableViewController {
@IBAction func unwindFromBolusViewController(_ segue: UIStoryboardSegue) {
if let bolusViewController = segue.source as? BolusViewController {
+ guard let ti = treatmentInformation, ti.allowed else {
+
+ //self.presentAlertController(
+ self.bolusState = nil
+ return
+ }
+
if let bolus = bolusViewController.bolus, bolus > 0 {
self.bolusState = .enacting
deviceManager.enactBolus(units: bolus) { (_) in
@@ -932,8 +1422,14 @@ final class StatusTableViewController: ChartsTableViewController {
if workoutMode == true {
deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.clearOverride(matching: .workout)
} else {
- let vc = UIAlertController(workoutDurationSelectionHandler: { (endDate) in
- _ = self.deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(.workout, until: endDate)
+ let vc = UIAlertController(workoutDurationSelectionHandler: { (endDate, disconnect) in
+ if disconnect {
+ self.deviceManager.loopManager.enablePumpDetachedMode()
+ } else {
+ _ = self.deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(.workout, until: endDate)
+ self.workoutMode = true
+ }
+
})
present(vc, animated: true, completion: nil)
@@ -987,4 +1483,180 @@ final class StatusTableViewController: ChartsTableViewController {
UIApplication.shared.open(url)
}
}
+
+ // MODIFICATIONS
+
+ // CARB / BOLUS NOTIFICATION
+ private var treatmentInformation: TreatmentInformation?
+ private var treatmentDisplayDismissed = false
+ // what the data view currently displays
+ private var displayTreatmentInformation: TreatmentInformation?
+
+ // MANUAL GLUCOSE ENTRY
+ private var validGlucose : GlucoseValue? = nil
+ private var needManualGlucose : Date? = nil
+ private var displayNeedManualGlucose : Date? = nil
+
+ // DETACHED MODE
+ private var pumpDetachedMode : Date?
+ private var displayPumpDetachedMode : Date?
+
+ // EXPERT MODE
+ private var expertMode : Bool = false
+ private var settingsTouchTime : Date? = nil
+ @objc func toggleExpertMode(_ sender: UILongPressGestureRecognizer) {
+ guard let toolbar = navigationController?.toolbar else {
+ return
+ }
+ let location = sender.location(in: toolbar)
+ let width = toolbar.frame.width
+
+ if location.x > width/5 {
+ if sender.state == .began {
+ settingsTouchTime = Date()
+ }
+ if sender.state == .ended, let duration = settingsTouchTime?.timeIntervalSinceNow {
+ if abs(duration) > TimeInterval(2) {
+ expertMode = !expertMode
+ deviceManager.loopManager.addInternalNote("toggleExpertMode \(expertMode)")
+ toolbarItems![8].isEnabled = expertMode
+ if expertMode {
+ DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(minutes: 30)) {
+ self.expertMode = false
+ self.toolbarItems![8].isEnabled = self.expertMode
+ }
+ }
+ } else {
+ if !expertMode {
+ presentAlertController(withTitle: "Hint", message: "Press for 2 seconds to toggle expert mode.")
+ } else {
+ // performSegue(withIdentifier: SettingsTableViewController.className, sender: nil)
+ }
+ }
+ }
+ }
+ }
+
+ // Notes
+ @IBAction func unwindFromNoteTableViewController(_ segue: UIStoryboardSegue) {
+ if let controller = segue.source as? NoteTableViewController, controller.saved {
+ let note = controller.text
+ deviceManager.loopManager.addNote(note)
+ }
+ }
+
+ // QuickCarbEntry
+ @IBAction func unwindFromQuickCarbEntry(_ segue: UIStoryboardSegue) {
+ if let carbVC = segue.source as? QuickCarbEntryViewController, carbVC.saved {
+ var foodPick : FoodPick?
+ if let carbVCcarbs = carbVC.carbs {
+ let carbs = carbVCcarbs.quantity.doubleValue(for: HKUnit.gram())
+ let title = "QuickCarbEntry \(carbVC.noteEntered)"
+ let foodItem = FoodItem(carbRatio: 1.0, portionSize: carbs, absorption: .normal, title: title)
+ foodPick = FoodPick(item: foodItem, ratio: 1, date: carbVCcarbs.startDate)
+ }
+ handleFoodPick(foodPick, carbVC.glucose)
+ }
+ }
+
+ private func handleFoodPick(_ foodPick : FoodPick?, _ updatedGlucose : HKQuantity?) {
+ if let carbEntry = foodPick?.carbEntry {
+
+ updateCarbEntry(updatedEntry: carbEntry)
+ }
+
+ if let glucoseStore = deviceManager.loopManager.glucoseStore {
+ if let glucoseEntry = updatedGlucose {
+ glucoseStore.addGlucose(glucoseEntry, date: Date(), isDisplayOnly: false, device: nil) { (success, _, error) in
+
+ if error != nil {
+ NSLog("handleFoodPick: addGlucose error \(error as Any)")
+ }
+ }
+ let g = Int(glucoseEntry.doubleValue(for: HKUnit.milligramsPerDeciliter()))
+ NSLog("handleFoodPick: Adding glucose to Nightscout \(g) mg/dl")
+ deviceManager.loopManager.addBGReceived(bloodGlucose: g, comment: "Manually Entered")
+ } else {
+ // no glucose entry given
+ }
+ }
+ }
+
+ @IBAction func unwindFromNewFoodPickerViewController(_ segue: UIStoryboardSegue) {
+ if let controller = segue.source as? NewFoodPickerViewController, let pick = controller.foodPick {
+ handleFoodPick(pick, nil)
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ // self.needsRefresh = true
+ self.reloadData()
+ }
+ }
+ }
+
+ private var foodRecentCollectionViewDataSource = FoodRecentCollectionViewDataSource()
+ private var displayMeal : Bool = true
+ weak var foodManager: FoodManager!
+ typealias MealInformation = (date: Date, lastCarbEntry: CarbEntry?, picks: FoodPicks?, start: Date?, end: Date?, carbs: Double?, undoPossible: Bool)
+ private var mealInformation : MealInformation?
+
+ func mealTableViewCellTap(_ sender : MealTableViewCell) {
+ // performSegue(withIdentifier: FoodPickerViewController.className, sender: sender)
+ performSegue(withIdentifier: NewFoodPickerViewController.className, sender: sender)
+ }
+
+ func mealTableViewCellImageTap(_ sender : MealTableViewCell) {
+ if let mi = self.mealInformation, let lastCarbEntry = mi.lastCarbEntry, let pick = lastCarbEntry.foodPicks().last, mi.undoPossible {
+
+ let alert = UIAlertController(title: "Undo Food Selection", message: "Are you sure you want to remove the last food pick \(pick.item.title) of \(pick.displayCarbs) g carbs?", preferredStyle: .alert)
+
+
+ alert.addAction(UIAlertAction(title: "Remove", style: .default, handler: { [weak alert] (_) in
+ NSLog("removeLastFoodPick Alert \(alert as Any)")
+ self.deviceManager.loopManager.removeCarbEntry(carbEntry: lastCarbEntry) { (error) in
+ if let err = error {
+ NSLog("removeLastFoodPick Error \(err as Any)")
+ self.presentAlertController(with: err)
+ }
+ self.reloadData()
+ }
+ }))
+
+ alert.addAction(UIAlertAction(title: "Back", style: .cancel, handler: nil))
+ self.present(alert, animated: true, completion: nil)
+
+ } else {
+ // performSegue(withIdentifier: FoodPickerViewController.className, sender: sender)
+ performSegue(withIdentifier: NewFoodPickerViewController.className, sender: sender)
+
+ }
+ }
+
+ private lazy var timeFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .none
+ formatter.timeStyle = .short
+
+ return formatter
+ }()
+
+ @objc func showNote(_ sender: UIBarButtonItem) {
+ performSegue(withIdentifier: NoteTableViewController.className, sender: sender)
+ }
+
+ @objc func showQuickCarbEntry(_ sender: UIBarButtonItem) {
+ performSegue(withIdentifier: QuickCarbEntryViewController.className, sender: sender)
+ }
+
+ private func createNoteButtonItem() -> UIBarButtonItem {
+ let originalImage = #imageLiteral(resourceName: "pencil")
+ let scaledIcon = UIImage(cgImage: originalImage.cgImage!, scale: 8, orientation: originalImage.imageOrientation)
+
+ let item = UIBarButtonItem(image: scaledIcon, style: .plain, target: self, action: #selector(showNote(_:)))
+ item.accessibilityLabel = NSLocalizedString("Note Taking", comment: "The label of the note taking button")
+
+ item.tintColor = UIColor(red: 249.0/255, green: 229.0/255, blue: 0.0/255, alpha: 1.0)
+
+ return item
+ }
+
}
diff --git a/Loop/View Controllers/TextFieldTableViewController.swift b/Loop/View Controllers/TextFieldTableViewController.swift
index 96bb4e433b..bc5f6c1270 100644
--- a/Loop/View Controllers/TextFieldTableViewController.swift
+++ b/Loop/View Controllers/TextFieldTableViewController.swift
@@ -72,5 +72,19 @@ extension TextFieldTableViewController {
}
return vc
- }
+ }
+
+ static func maxInsulinOnBoard(_ value: Double?) -> T {
+ let vc = T()
+
+ vc.placeholder = NSLocalizedString("Enter a number of units", comment: "The placeholder text instructing users how to enter a maximum iob")
+ vc.keyboardType = .decimalPad
+ vc.unit = NSLocalizedString("Units", comment: "The unit string for units")
+
+ if let maxIOB = value {
+ vc.value = valueNumberFormatter.string(from: NSNumber(value: maxIOB))
+ }
+
+ return vc
+ }
}
diff --git a/Loop/Views/FoodCollectionReusableView.swift b/Loop/Views/FoodCollectionReusableView.swift
new file mode 100644
index 0000000000..5174715151
--- /dev/null
+++ b/Loop/Views/FoodCollectionReusableView.swift
@@ -0,0 +1,15 @@
+//
+// FoodCollectionReusableView.swift
+// Loop
+//
+// Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+final class FoodCollectionReusableView : UICollectionReusableView {
+
+ @IBOutlet weak var headerLabel: UILabel!
+
+}
diff --git a/Loop/Views/FoodCollectionViewCell.swift b/Loop/Views/FoodCollectionViewCell.swift
new file mode 100644
index 0000000000..b2f181c741
--- /dev/null
+++ b/Loop/Views/FoodCollectionViewCell.swift
@@ -0,0 +1,28 @@
+//
+// FoodCollectionViewCell.swift
+// Loop
+//
+// Copyright © 2017 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+final class FoodCollectionViewCell: UICollectionViewCell {
+
+ @IBOutlet weak var imageView: UIImageView!
+ @IBOutlet weak var foodLabel: UILabel?
+
+ @IBOutlet weak var carbLabel: UILabel?
+
+ override func prepareForReuse() {
+ self.imageView.image = nil
+ self.backgroundColor = UIColor.lightGray
+ if self.foodLabel != nil {
+ self.foodLabel!.text = "???"
+ }
+ if self.carbLabel != nil {
+ self.carbLabel!.text = ""
+ }
+ }
+}
diff --git a/Loop/Views/FoodRecentCollectionView.swift b/Loop/Views/FoodRecentCollectionView.swift
new file mode 100644
index 0000000000..62aee1ba8c
--- /dev/null
+++ b/Loop/Views/FoodRecentCollectionView.swift
@@ -0,0 +1,65 @@
+import UIKit
+
+final class FoodRecentPickerFlowLayout: UICollectionViewFlowLayout {
+
+ override init() {
+ super.init()
+ setupLayout()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupLayout()
+ }
+
+ func setupLayout() {
+ minimumInteritemSpacing = 1
+ minimumLineSpacing = 1
+ scrollDirection = .horizontal
+ }
+
+ override var itemSize: CGSize {
+ set {
+
+ }
+ get {
+ let itemHeight = self.collectionView!.frame.height
+ return CGSize(width: itemHeight, height: itemHeight)
+ }
+ }
+}
+
+class FoodRecentCollectionViewDataSource : NSObject, UICollectionViewDataSource {
+
+
+ var foodPicks : FoodPicks = FoodPicks()
+ var foodManager : FoodManager? = nil
+
+
+ func numberOfSections(in collectionView: UICollectionView) -> Int {
+ return 1
+ }
+
+
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+
+ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "foodRecentImageCell" , for: indexPath) as! FoodCollectionViewCell
+
+
+ let items = foodPicks.picks
+ let pick = items[indexPath.item]
+ cell.imageView.layer.masksToBounds = true
+ cell.imageView.clipsToBounds = true
+ cell.imageView.image = foodManager?.image(pick: pick)
+ let carbs = Int(round(pick.carbs))
+ cell.carbLabel?.text = "\(carbs)"
+ return cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+
+ return foodPicks.picks.count
+ }
+
+
+}
diff --git a/Loop/Views/MealTableViewCell.swift b/Loop/Views/MealTableViewCell.swift
new file mode 100644
index 0000000000..93b88c85a9
--- /dev/null
+++ b/Loop/Views/MealTableViewCell.swift
@@ -0,0 +1,63 @@
+//
+// TitleSubtitleTableViewCell.swift
+// Loop
+//
+// Created by Nate Racklyeft on 9/28/16.
+// Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import UIKit
+
+class MealTableViewCell: UITableViewCell {
+
+
+ @IBOutlet weak var currentCarbLabel: UILabel!
+
+ @IBOutlet weak var currentCarbDate: UILabel!
+
+ @IBOutlet weak var undoLabel: UILabel!
+ @IBOutlet weak var leftImageView: UIImageView!
+
+ @IBOutlet weak var lastItemView: UIImageView!
+ @IBOutlet weak var recentFoodCollectionView: UICollectionView!
+
+ @IBOutlet weak var debugLabelTop: UILabel!
+ @IBOutlet weak var debugLabelBottom: UILabel!
+
+ @objc func tapCell(_ sender: UITapGestureRecognizer) {
+ if sender.state != .ended {
+ return
+ }
+ let location = sender.location(in: self)
+ let width = self.frame.width
+
+ NSLog("tapCell \(location.x) \(location.y) \(width)")
+
+ if location.y > 60 {
+ return
+ }
+ if location.x < (self.frame.width - undoLabel.frame.width) {
+ self.delegate?.mealTableViewCellTap(self)
+ } else {
+ self.delegate?.mealTableViewCellImageTap(self)
+
+ }
+ }
+
+ var delegate: MealTableViewCellDelegate?
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapCell(_:)))
+ addGestureRecognizer(tapGesture)
+ //tapGesture.delegate = ViewController()
+
+ }
+
+}
+
+protocol MealTableViewCellDelegate {
+ func mealTableViewCellImageTap(_ sender : MealTableViewCell)
+ func mealTableViewCellTap(_ sender : MealTableViewCell)
+
+}
diff --git a/Loop/Views/TitleSubtitleTableViewCell.swift b/Loop/Views/TitleSubtitleTableViewCell.swift
index ab993a3d15..8340f9b377 100644
--- a/Loop/Views/TitleSubtitleTableViewCell.swift
+++ b/Loop/Views/TitleSubtitleTableViewCell.swift
@@ -17,7 +17,9 @@ class TitleSubtitleTableViewCell: UITableViewCell {
subtitleLabel.textColor = UIColor.secondaryLabelColor
}
}
-
+
+ @IBOutlet weak var explanationLabel: UILabel!
+
override func layoutSubviews() {
super.layoutSubviews()
diff --git a/LoopTests/Info.plist b/LoopTests/Info.plist
index 6d86f6a299..ba8d5550a2 100644
--- a/LoopTests/Info.plist
+++ b/LoopTests/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.5.6
+ 1.9.4.20200329
CFBundleSignature
????
CFBundleVersion
diff --git a/LoopUI/Info.plist b/LoopUI/Info.plist
index 6d29dddc6f..37d6a3281b 100644
--- a/LoopUI/Info.plist
+++ b/LoopUI/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 1.5.6
+ 1.9.4.20200329
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPrincipalClass
diff --git a/LoopUI/Views/GlucoseHUDView.swift b/LoopUI/Views/GlucoseHUDView.swift
index 59b3b8ee3a..00f4a3db63 100644
--- a/LoopUI/Views/GlucoseHUDView.swift
+++ b/LoopUI/Views/GlucoseHUDView.swift
@@ -95,7 +95,11 @@ public final class GlucoseHUDView: BaseHUDView {
let numberFormatter = NumberFormatter.glucoseFormatter(for: unit)
if let valueString = numberFormatter.string(from: NSNumber(value: glucoseQuantity)) {
- glucoseLabel.text = valueString
+ if glucoseStartDate.timeIntervalSinceNow > -TimeInterval(minutes: 15) {
+ glucoseLabel.text = valueString
+ } else {
+ glucoseLabel.text = "-"
+ }
accessibilityStrings.append(String(format: NSLocalizedString("%1$@ at %2$@", comment: "Accessbility format value describing glucose: (1: glucose number)(2: glucose time)"), valueString, time))
}
diff --git a/README.md b/README.md
index b7680a9698..6014708ea2 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,12 @@
+# IMPORTANT
+
+This is the [ErikDi](https://github.com/erikdi/Loop) Loop fork.
+
+See [FEATURES](/FEATURES.md) for a list of new features and [TODO](/TODO.md) for planned stuff.
+
+Use at your own risk.
+
+
# Loop for iOS

diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000000..d8f42095b4
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,50 @@
+# TODO
+
+## Foodpicker
+
+- Bug: Undo carbs does not work with a weird healthd error. Very similar
+ code path seems to work fine in Carb Counteraction View Controller.
+- Bug: Undo possible even after bolus
+- Feature: Edit amount after the fact.
+- UI: Instead of carbs display selected quantity.
+
+## Future Low Warning
+
+- Add different sounds depending on urgency of eating.
+- Use the same code path as the Bolus calculation.
+
+## Activity
+
+- Feature: Log workout mode and disconnect as Exercise in Nightscout (requires
+ tracking the ID of the event from Nightscout).
+
+## QuickCarbEntry
+
+- UI: Display slider or wheel for carbs instead of text entry
+
+## Safety Carbs-Entered / Considered limit.
+
+- Feature: could probably simplify to a warning threshold and refuse entry above a certain amount
+ Not super important, as MaxIOB is doing the same thing essentially
+
+## Consistent placement of "Save" button
+
+- Nit: currently 'Pick Food' is the only one with a button on the top right, should probably get one some at the bottom
+ or Notes should have a save button on the top right.
+
+## Bluetooth restart
+
+- Still needs a lot of testing and a way to trigger without Bluetooth packets.
+- February: Seems to work fine now.
+
+## Foodmanager
+
+- Add dextrose tabs
+- Add ice cream
+- Reduce milk and chocolate default to 200 ml
+- Check if fries carbs are correct.
+
+
+## Nightscout Logging
+
+- Limit backlog to prevent crashes in no connectivity situations.
diff --git a/WatchApp Extension/Info.plist b/WatchApp Extension/Info.plist
index b550c08541..5f75739e04 100644
--- a/WatchApp Extension/Info.plist
+++ b/WatchApp Extension/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- 1.5.6
+ 1.9.4.20200329
CFBundleSignature
????
CFBundleVersion
diff --git a/WatchApp/Info.plist b/WatchApp/Info.plist
index 4123828ce1..0810d2b24b 100644
--- a/WatchApp/Info.plist
+++ b/WatchApp/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
en
CFBundleDisplayName
- Loop
+ Loop2
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.5.6
+ 1.9.4.20200329
CFBundleSignature
????
CFBundleVersion
diff --git a/version_update.sh b/version_update.sh
new file mode 100644
index 0000000000..52f3d9218b
--- /dev/null
+++ b/version_update.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+# Simple script to update the version in all relevant targets.
+# Usage:
+# bash version_update.sh 1.9.4.20190824 1.9.4.20190825
+
+set +e
+
+OLDVERSION="$1"
+NEWVERSION="$2"
+
+if [ "$NEWVERSION" == "" ]; then
+ NEWVERSION="$OLDVERSION"
+ OLDVERSION=$(egrep -o '1\.9\.4\.\d+' Loop/Info.plist)
+ echo "Automatically set old version $OLDVERSION"
+ if [ "$NEWVERSION" == "" ]; then
+ NEWVERSION=1.9.4.$(date +%Y%m%d)
+ echo "Automatically set new version $NEWVERSION"
+ fi
+fi
+
+case "$NEWVERSION" in
+ "")
+ echo "new version cannot be empty"
+ exit 2;;
+ [0-9].[0-9].[0-9].20[1-2][0-9][0-1][0-9][0-3][0-9])
+ echo new version is ok;;
+ *)
+ echo "new version is not ok"
+ exit 2;;
+esac
+
+case "$OLDVERSION" in
+ "")
+ echo "old version cannot be empty"
+ exit 3;;
+ [0-9].[0-9].[0-9].20[1-2][0-9][0-1][0-9][0-3][0-9])
+ echo old version is ok;;
+ *)
+ echo "old version is ok, but non-standard"
+esac
+
+for f in "Loop/Info.plist" "LoopUI/Info.plist" "WatchApp Extension/Info.plist" "WatchApp/Info.plist" "DoseMathTests/Info.plist" "LoopTests/Info.plist" "Loop Status Extension/Info.plist" ; do
+
+ sed -i "" "s/>$OLDVERSION>$NEWVERSION" "$f"
+ git add "$f"
+done
+
+PROJECTVERSION=$(sed -n -E "s/CURRENT_PROJECT_VERSION = ([0-9]+);/\1 + 1/p" Loop.xcodeproj/project.pbxproj | head -n1 | bc)
+echo "New project version $PROJECTVERSION"
+sed -E -i "" "s/(CURRENT_PROJECT|DYLIB_CURRENT)(_VERSION =)( +[0-9]+)/\1\2 $PROJECTVERSION/" Loop.xcodeproj/project.pbxproj
+git add Loop.xcodeproj/project.pbxproj
+git commit -m "Update to version $PROJECTVERSION"
+git tag "v$NEWVERSION.$PROJECTVERSION"