소스 검색

Libre transmitter source. (Not tested!)

Ivan Valkou 4 년 전
부모
커밋
a92d66196c

+ 57 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -180,6 +180,9 @@
 		38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3F92737E42000574A46 /* BaseStateModel.swift */; };
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
 		38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMProvider.swift */; };
+		38FEF408273B011A00574A46 /* LibreTransmitterSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */; };
+		38FEF40F273B2BDA00574A46 /* LibreTransmitter in Frameworks */ = {isa = PBXBuildFile; productRef = 38FEF40E273B2BDA00574A46 /* LibreTransmitter */; };
+		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A965332F237348B119FB858 /* PreferencesEditorRootView.swift */; };
 		448B6FCB252BD4796E2960C0 /* PumpSettingsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0274EE6439B1C3ED70730D41 /* PumpSettingsEditorDataFlow.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
@@ -188,12 +191,14 @@
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
 		5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE53A13D26F101B332EFFC8 /* AddTempTargetProvider.swift */; };
 		5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */; };
+		61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */; };
 		63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */; };
 		642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */; };
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
 		69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */; };
 		6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */; };
+		6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
@@ -202,6 +207,8 @@
 		891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF98E22A39CD656A230704 /* AutotuneConfigProvider.swift */; };
 		8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */; };
 		8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */; };
+		903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */; };
+		9050F378F0063C064D7FFC86 /* LibreConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */; };
 		919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C3B5FD881CA45DFDEE0EDA9 /* AddTempTargetStateModel.swift */; };
 		9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */; };
 		9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A48AE3AC813A49A517846A /* NightscoutConfigStateModel.swift */; };
@@ -456,6 +463,8 @@
 		38FEF3F92737E42000574A46 /* BaseStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FD2738083E00574A46 /* CGMProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMProvider.swift; sourceTree = "<group>"; };
+		38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTransmitterSource.swift; sourceTree = "<group>"; };
+		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		39E7C997E56DAF8D28D09014 /* AddCarbsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsStateModel.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
@@ -472,6 +481,7 @@
 		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
 		618E62C9757B2F95431B5DC0 /* AddCarbsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorStateModel.swift; sourceTree = "<group>"; };
+		66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigDataFlow.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
 		680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = "<group>"; };
 		6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorProvider.swift; sourceTree = "<group>"; };
@@ -500,6 +510,7 @@
 		B8C7F882606FF83A21BE00D8 /* PumpSettingsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorRootView.swift; sourceTree = "<group>"; };
 		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
+		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
@@ -514,6 +525,8 @@
 		E00EEC0027368630002FF094 /* UIAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0127368630002FF094 /* APSAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0227368630002FF094 /* NetworkAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = "<group>"; };
+		E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigProvider.swift; sourceTree = "<group>"; };
+		E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = "<group>"; };
 		E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = "<group>"; };
 		FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorRootView.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -523,6 +536,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				38FEF40F273B2BDA00574A46 /* LibreTransmitter in Frameworks */,
 				38887DF225F61F7500944304 /* NightscoutUploadKit.framework in Frameworks */,
 				38887DE825F61F7500944304 /* MockKitUI.framework in Frameworks */,
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
@@ -630,6 +644,7 @@
 				9E56E3626FAD933385101B76 /* DataTable */,
 				3811DE2725C9D49500A708ED /* Home */,
 				D8F047E14D567F2B5DBEFD96 /* ISFEditor */,
+				C11D545CED3ECEB525EDEE23 /* LibreConfig */,
 				3811DE1A25C9D48300A708ED /* Main */,
 				5031FE61F63C2A8A8B7674DD /* ManualTempBasal */,
 				D533BF261CDC1C3F871E7BFD /* NightscoutConfig */,
@@ -882,6 +897,7 @@
 				38569344270B5DFA0002C50D /* CGMType.swift */,
 				386A124E271707F000DDC61C /* DexcomSource.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
+				38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */,
 			);
 			path = CGM;
 			sourceTree = "<group>";
@@ -972,6 +988,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				38FEF412273B317A00574A46 /* HKUnit.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
@@ -1275,6 +1292,14 @@
 			path = BasalProfileEditor;
 			sourceTree = "<group>";
 		};
+		A56097CB1DCBCE98F2F42177 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		A9A4C88374496B3C89058A89 /* AddTempTarget */ = {
 			isa = PBXGroup;
 			children = (
@@ -1294,6 +1319,17 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		C11D545CED3ECEB525EDEE23 /* LibreConfig */ = {
+			isa = PBXGroup;
+			children = (
+				66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */,
+				E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */,
+				E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */,
+				A56097CB1DCBCE98F2F42177 /* View */,
+			);
+			path = LibreConfig;
+			sourceTree = "<group>";
+		};
 		C2C98283C436DB934D7E7994 /* Bolus */ = {
 			isa = PBXGroup;
 			children = (
@@ -1405,6 +1441,7 @@
 				38B17B6525DD90E0005CAE3D /* SwiftDate */,
 				3833B46C26012030003021B3 /* Algorithms */,
 				38192E00261B826A0094D973 /* Alamofire */,
+				38FEF40E273B2BDA00574A46 /* LibreTransmitter */,
 			);
 			productName = FreeAPS;
 			productReference = 388E595825AD948C0019842D /* FreeAPS.app */;
@@ -1480,6 +1517,7 @@
 				38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */,
 				3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
 				38192DFF261B826A0094D973 /* XCRemoteSwiftPackageReference "Alamofire" */,
+				38FEF40D273B2BDA00574A46 /* XCRemoteSwiftPackageReference "LibreTransmitterX" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -1553,6 +1591,7 @@
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
+				38FEF408273B011A00574A46 /* LibreTransmitterSource.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				386A124F271707F000DDC61C /* DexcomSource.swift in Sources */,
@@ -1682,6 +1721,7 @@
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
 				17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */,
@@ -1728,6 +1768,10 @@
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
 				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
+				61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */,
+				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
+				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,
+				9050F378F0063C064D7FFC86 /* LibreConfigRootView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -2069,6 +2113,14 @@
 				minimumVersion = 6.3.1;
 			};
 		};
+		38FEF40D273B2BDA00574A46 /* XCRemoteSwiftPackageReference "LibreTransmitterX" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/ivalkou/LibreTransmitterX";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.0.0;
+			};
+		};
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
@@ -2097,6 +2149,11 @@
 			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;
 			productName = SwiftDate;
 		};
+		38FEF40E273B2BDA00574A46 /* LibreTransmitter */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 38FEF40D273B2BDA00574A46 /* XCRemoteSwiftPackageReference "LibreTransmitterX" */;
+			productName = LibreTransmitter;
+		};
 /* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 388E595025AD948C0019842D /* Project object */;

+ 9 - 0
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -20,6 +20,15 @@
         }
       },
       {
+        "package": "LibreTransmitter",
+        "repositoryURL": "https://github.com/ivalkou/LibreTransmitterX",
+        "state": {
+          "branch": null,
+          "revision": "f9591eb04bd812d0fae8f66d8a863e7cbcf2af9b",
+          "version": "1.0.3"
+        }
+      },
+      {
         "package": "swift-algorithms",
         "repositoryURL": "https://github.com/apple/swift-algorithms",
         "state": {

+ 11 - 0
FreeAPS/Resources/Info.plist

@@ -18,6 +18,17 @@
 	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
 	<key>CFBundleShortVersionString</key>
 	<string>$(MARKETING_VERSION)</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>freeaps-x</string>
+			</array>
+		</dict>
+	</array>
 	<key>CFBundleVersion</key>
 	<string>$(BUILD_VERSION)</string>
 	<key>ITSAppUsesNonExemptEncryption</key>

+ 5 - 0
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -7,6 +7,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     case xdrip
     case dexcomG6
     case dexcomG5
+    case libreTransmitter
 
     var displayName: String {
         switch self {
@@ -18,6 +19,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return "Dexcom G6"
         case .dexcomG5:
             return "Dexcom G5"
+        case .libreTransmitter:
+            return "Libre Transmitter"
         }
     }
 
@@ -31,6 +34,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return URL(string: "dexcomg6://")!
         case .dexcomG5:
             return URL(string: "dexcomgcgm://")!
+        case .libreTransmitter:
+            return URL(string: "freeaps-x://libre-transmitter")!
         }
     }
 }

+ 98 - 0
FreeAPS/Sources/APS/CGM/LibreTransmitterSource.swift

@@ -0,0 +1,98 @@
+import Combine
+import Foundation
+import LibreTransmitter
+import Swinject
+
+protocol LibreTransmitterSource: GlucoseSource {
+    var manager: LibreTransmitterManager? { get set }
+}
+
+final class BaseLibreTransmitterSource: LibreTransmitterSource, Injectable {
+    private let processQueue = DispatchQueue(label: "BaseLibreTransmitterSource.processQueue")
+
+    @Injected() var glucoseStorage: GlucoseStorage!
+
+    private var promise: Future<[BloodGlucose], Error>.Promise?
+
+    var manager: LibreTransmitterManager? {
+        didSet {
+            configured = manager != nil
+            manager?.cgmManagerDelegate = self
+        }
+    }
+
+    @Persisted(key: "LibreTransmitterManager.configured") private(set) var configured = false
+
+    init(resolver: Resolver) {
+        if configured {
+            manager = LibreTransmitterManager()
+            manager?.cgmManagerDelegate = self
+        }
+
+        injectServices(resolver)
+    }
+
+    func fetch() -> AnyPublisher<[BloodGlucose], Never> {
+        Future<[BloodGlucose], Error> { [weak self] promise in
+            self?.promise = promise
+        }
+        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
+        .replaceError(with: [])
+        .eraseToAnyPublisher()
+    }
+}
+
+extension BaseLibreTransmitterSource: LibreTransmitterManagerDelegate {
+    var queue: DispatchQueue { processQueue }
+
+    func startDateToFilterNewData(for _: LibreTransmitterManager) -> Date? {
+        glucoseStorage.syncDate()
+    }
+
+    func cgmManager(_ manager: LibreTransmitterManager, hasNew result: Result<[LibreGlucose], Error>) {
+        switch result {
+        case let .success(newGlucose):
+            let glucose = newGlucose.map { value -> BloodGlucose in
+                BloodGlucose(
+                    _id: value.syncId,
+                    sgv: Int(value.glucose),
+                    direction: manager.glucoseDisplay?.trendType
+                        .map { .init(trendType: $0) },
+                    date: Decimal(Int(value.startDate.timeIntervalSince1970 * 1000)),
+                    dateString: value.startDate,
+                    filtered: nil,
+                    noise: nil,
+                    glucose: Int(value.glucose),
+                    type: "sgv"
+                )
+            }
+
+            promise?(.success(glucose))
+
+        case let .failure(error):
+            warning(.service, "LibreTransmitter error:", error: error)
+            promise?(.failure(error))
+        }
+    }
+}
+
+extension BloodGlucose.Direction {
+    init(trendType: GlucoseTrend) {
+        switch trendType {
+        case .upUpUp:
+            self = .doubleUp
+        case .upUp:
+            self = .singleUp
+        case .up:
+            self = .fortyFiveUp
+        case .flat:
+            self = .flat
+        case .down:
+            self = .fortyFiveDown
+        case .downDown:
+            self = .singleDown
+        case .downDownDown:
+            self = .doubleDown
+        }
+    }
+}

+ 7 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -11,6 +11,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var apsManager: APSManager!
     @Injected() var settingsManager: SettingsManager!
+    @Injected() var libreTransmitter: LibreTransmitterSource!
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
@@ -36,6 +37,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         case .nightscout,
              .none:
             glucoseSource = nightscoutManager
+        case .libreTransmitter:
+            glucoseSource = libreTransmitter
+        }
+
+        if settingsManager.settings.cgm != .libreTransmitter {
+            libreTransmitter.manager = nil
         }
     }
 

+ 1 - 0
FreeAPS/Sources/Assemblies/APSAssembly.swift

@@ -8,5 +8,6 @@ final class APSAssembly: Assembly {
         container.register(FetchGlucoseManager.self) { r in BaseFetchGlucoseManager(resolver: r) }
         container.register(FetchTreatmentsManager.self) { r in BaseFetchTreatmentsManager(resolver: r) }
         container.register(FetchAnnouncementsManager.self) { r in BaseFetchAnnouncementsManager(resolver: r) }
+        container.register(LibreTransmitterSource.self) { r in BaseLibreTransmitterSource(resolver: r) }
     }
 }

+ 58 - 0
FreeAPS/Sources/Helpers/HKUnit.swift

@@ -0,0 +1,58 @@
+import HealthKit
+
+extension HKUnit {
+    static let milligramsPerDeciliter: HKUnit = {
+        HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
+    }()
+
+    static let millimolesPerLiter: HKUnit = {
+        HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
+    }()
+
+    static let internationalUnitsPerHour: HKUnit = {
+        HKUnit.internationalUnit().unitDivided(by: .hour())
+    }()
+
+    static let gramsPerUnit: HKUnit = {
+        HKUnit.gram().unitDivided(by: .internationalUnit())
+    }()
+
+    var foundationUnit: Unit? {
+        if self == HKUnit.milligramsPerDeciliter {
+            return UnitConcentrationMass.milligramsPerDeciliter
+        }
+
+        if self == HKUnit.millimolesPerLiter {
+            return UnitConcentrationMass.millimolesPerLiter(withGramsPerMole: HKUnitMolarMassBloodGlucose)
+        }
+
+        if self == HKUnit.gram() {
+            return UnitMass.grams
+        }
+
+        return nil
+    }
+
+    /// The smallest value expected to be visible on a chart
+    var chartableIncrement: Double {
+        if self == .milligramsPerDeciliter {
+            return 1
+        } else {
+            return 1 / 25
+        }
+    }
+
+    var localizedShortUnitString: String {
+        if self == HKUnit.millimolesPerLiter {
+            return NSLocalizedString("mmol/L", comment: "The short unit display string for millimoles of glucose per liter")
+        } else if self == .milligramsPerDeciliter {
+            return NSLocalizedString("mg/dL", comment: "The short unit display string for milligrams of glucose per decilter")
+        } else if self == .internationalUnit() {
+            return NSLocalizedString("U", comment: "The short unit display string for international units of insulin")
+        } else if self == .gram() {
+            return NSLocalizedString("g", comment: "The short unit display string for grams")
+        } else {
+            return String(describing: self)
+        }
+    }
+}

+ 1 - 1
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -22,7 +22,7 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
     }
 
     var sgv: Int?
-    let direction: Direction?
+    var direction: Direction?
     let date: Decimal
     let dateString: Date
     let filtered: Decimal?

+ 3 - 1
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -3,6 +3,7 @@ import SwiftUI
 extension CGM {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var settingsManager: SettingsManager!
+        @Injected() var libreSource: LibreTransmitterSource!
 
         @Published var cgm: CGMType = .nightscout
         @Published var transmitterID = ""
@@ -16,7 +17,8 @@ extension CGM {
             $cgm
                 .removeDuplicates()
                 .sink { [weak self] value in
-                    self?.settingsManager.settings.cgm = value
+                    guard let self = self else { return }
+                    self.settingsManager.settings.cgm = value
                 }
                 .store(in: &lifetime)
 

+ 6 - 0
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -30,6 +30,12 @@ extension CGM {
                     }
                 }
 
+                if state.cgm == .libreTransmitter {
+                    Button("Configure Libre Transmitter") {
+                        state.showModal(for: .libreConfig)
+                    }
+                }
+
                 Section(header: Text("Other")) {
                     Toggle("Upload glucose to Nightscout", isOn: $state.uploadGlucose)
                 }

+ 2 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -274,6 +274,8 @@ extension Home {
                 url = URL(string: "spikeapp://")!
             case "http://127.0.0.1:17580":
                 url = URL(string: "diabox://")!
+            case CGMType.libreTransmitter.appURL?.absoluteString:
+                showModal(for: .libreConfig)
             default: break
             }
             UIApplication.shared.open(url, options: [:], completionHandler: nil)

+ 5 - 0
FreeAPS/Sources/Modules/LibreConfig/LibreConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum LibreConfig {
+    enum Config {}
+}
+
+protocol LibreConfigProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/LibreConfig/LibreConfigProvider.swift

@@ -0,0 +1,3 @@
+extension LibreConfig {
+    final class Provider: BaseProvider, LibreConfigProvider {}
+}

+ 18 - 0
FreeAPS/Sources/Modules/LibreConfig/LibreConfigStateModel.swift

@@ -0,0 +1,18 @@
+import HealthKit
+import SwiftUI
+
+extension LibreConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var source: LibreTransmitterSource!
+        @Injected() var settingsManager: SettingsManager!
+
+        @Published var configured = false
+
+        var unit = HKUnit.millimolesPerLiter
+
+        override func subscribe() {
+            configured = source.manager != nil
+            unit = settingsManager.settings.units == .mmolL ? .millimolesPerLiter : .milligramsPerDeciliter
+        }
+    }
+}

+ 36 - 0
FreeAPS/Sources/Modules/LibreConfig/View/LibreConfigRootView.swift

@@ -0,0 +1,36 @@
+import LibreTransmitter
+import SwiftUI
+import Swinject
+
+extension LibreConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        var body: some View {
+            Group {
+                if state.configured, let manager = state.source.manager {
+                    LibreTransmitterSettingsView(
+                        manager: manager,
+                        glucoseUnit: state.unit
+                    ) {
+                        self.state.source.manager = nil
+                        self.state.configured = false
+                    } completion: {
+                        state.hideModal()
+                    }
+                } else {
+                    LibreTransmitterSetupView { manager in
+                        self.state.source.manager = manager
+                        self.state.configured = true
+                    } completion: {
+                        state.hideModal()
+                    }
+                }
+            }
+            .navigationBarTitle("")
+            .navigationBarHidden(true)
+            .onAppear(perform: configureView)
+        }
+    }
+}

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -21,6 +21,7 @@ enum Screen: Identifiable, Hashable {
     case autotuneConfig
     case dataTable
     case cgm
+    case libreConfig
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -66,6 +67,8 @@ extension Screen {
             DataTable.RootView(resolver: resolver)
         case .cgm:
             CGM.RootView(resolver: resolver)
+        case .libreConfig:
+            LibreConfig.RootView(resolver: resolver)
         }
     }