Ivan Valkou 4 лет назад
Родитель
Сommit
ea5597b7fb
100 измененных файлов с 10055 добавлено и 15 удалено
  1. 0 12
      Dependencies/CGMBLEKit/CGMBLEKit.xcodeproj/project.pbxproj
  2. 1 1
      Dependencies/CGMBLEKit/CGMBLEKit/TransmitterManager.swift
  3. 2 2
      Dependencies/CGMBLEKit/Common/HKUnit.swift
  4. 388 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample.xcodeproj/project.pbxproj
  5. 7 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  6. 8 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  7. 11 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Assets.xcassets/AccentColor.colorset/Contents.json
  8. 98 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Assets.xcassets/AppIcon.appiconset/Contents.json
  9. 6 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Assets.xcassets/Contents.json
  10. 66 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/ContentView.swift
  11. 56 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Info.plist
  12. 8 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/LibreTramsmitterExample.entitlements
  13. 17 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/LibreTramsmitterExampleApp.swift
  14. 6 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Preview Content/Preview Assets.xcassets/Contents.json
  15. 43 0
      Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/StateModel.swift
  16. 28 0
      Dependencies/LibreTransmitter/Package.swift
  17. 3 0
      Dependencies/LibreTransmitter/README.md
  18. 40 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/Info.plist
  19. 212 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Headers/RawGlucose-Swift.h
  20. 18 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Headers/RawGlucose.h
  21. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Info.plist
  22. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftdoc
  23. 8 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftinterface
  24. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftdoc
  25. 8 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftinterface
  26. 11 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/module.modulemap
  27. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/RawGlucose
  28. 201 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/_CodeSignature/CodeResources
  29. 430 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Headers/RawGlucose-Swift.h
  30. 18 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Headers/RawGlucose.h
  31. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Info.plist
  32. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftdoc
  33. 8 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftinterface
  34. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftdoc
  35. 8 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftinterface
  36. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftdoc
  37. 8 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftinterface
  38. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64.swiftdoc
  39. 8 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64.swiftinterface
  40. 11 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/module.modulemap
  41. BIN
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/RawGlucose
  42. 267 0
      Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/_CodeSignature/CodeResources
  43. 258 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/BluetoothSearch.swift
  44. 84 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/CBPeripheralExtensions.swift
  45. 111 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/GenericThrottler.swift
  46. 117 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/LibreTransmitterMetadata.swift
  47. 207 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/BubbleTransmitter.swift
  48. 158 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/Libre2DirectTransmitter.swift
  49. 729 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/LibreTransmitterProxyManager.swift
  50. 89 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/LibreTransmitterProxyProtocol.swift
  51. 337 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/MiaomiaoTransmitter.swift
  52. 20 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/UUIDContainer.swift
  53. 29 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/CollectionExtensions.swift
  54. 33 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/DataExtensions.swift
  55. 97 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/DateExtensions.swift
  56. 29 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/DoubleExtensions.swift
  57. 21 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/HashableClass.swift
  58. 41 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/TimeIntervalExtensions.swift
  59. 18 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/GlucoseSampleValue.swift
  60. 91 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/GlucoseValue.swift
  61. 27 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/HKQuantitySample+GlucoseKit.swift
  62. 67 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/HKUnit.swift
  63. 29 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/LocalizedString.swift
  64. 48 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/MessagePassing.swift
  65. 63 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NewGlucoseSample.swift
  66. 447 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NotificationHelper.swift
  67. 47 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NumberFormatter.swift
  68. 252 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/QuantityFormatter.swift
  69. 115 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/SampleValue.swift
  70. 226 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/GlucoseSchedules.swift
  71. 258 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/KeyChainManagerWrapper.swift
  72. 60 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/LogExport.swift
  73. 18 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UIApplication+metadata.swift
  74. 184 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+Alarmsettings.swift
  75. 48 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+Bluetooth.swift
  76. 35 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+GlucoseSettings.swift
  77. 38 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/UnfairLock.swift
  78. 83 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/WeakSynchronizedDelegate.swift
  79. 18 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/CompletionNotifying.swift
  80. 126 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/ConcreteGlucoseDisplayable.swift
  81. 24 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/DeviceLogEntryType.swift
  82. 43 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Features.swift
  83. 225 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreGlucose.swift
  84. 75 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/Calibration.swift
  85. 32 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/GlucoseAlgorithm/GlucoseFromRaw.swift
  86. 40 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/GlucoseAlgorithm/GlucoseSmoothing.swift
  87. 42 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/LibreError.swift
  88. 63 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/LimitedQueue.swift
  89. 105 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/CRC.swift
  90. 142 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/Measurement.swift
  91. 785 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/PreLibre2.swift
  92. 179 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/Sensor.swift
  93. 469 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorData.swift
  94. 107 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorSerialNumber.swift
  95. 69 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorState.swift
  96. 878 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterManager.swift
  97. 115 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Controllers/LibreTransmitterSetupViewController.swift
  98. BIN
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Graphics/bubble.png
  99. BIN
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Graphics/ic_bubble_mini_3-2.png
  100. 0 0
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Graphics/icons8-down-arrow-50.png

+ 0 - 12
Dependencies/CGMBLEKit/CGMBLEKit.xcodeproj/project.pbxproj

@@ -82,7 +82,6 @@
 		431CE7661F91D0B300255374 /* PeripheralManager+G5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PeripheralManager+G5.swift"; sourceTree = "<group>"; };
 		4323115E1EFC870300B95E62 /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
 		4325E9EC210EAEF500969CE5 /* TransmitterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransmitterManager.swift; sourceTree = "<group>"; };
-		4325E9EE210EAF3F00969CE5 /* ShareClientUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClientUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		433BC81A205CB64A000B1200 /* GlucoseBackfillMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBackfillMessage.swift; sourceTree = "<group>"; };
 		434B288220649D3C000EE07B /* ResetMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetMessage.swift; sourceTree = "<group>"; };
 		435535D31FB2C1B000CE5A23 /* PeripheralManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManagerError.swift; sourceTree = "<group>"; };
@@ -91,9 +90,6 @@
 		4379CFD5210EB19F00AADC79 /* Locked.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = "<group>"; };
 		43846AC51D8F896C00799272 /* CalibrationDataRxMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationDataRxMessage.swift; sourceTree = "<group>"; };
 		43880F971D9E19FC009061A8 /* TransmitterVersionRxMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransmitterVersionRxMessage.swift; sourceTree = "<group>"; };
-		43A8EC49210D09BE00A81379 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
-		43A8EC62210D4D1900A81379 /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
-		43A8EC70210E629300A81379 /* ShareClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		43CABDF31C3506F100005705 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		43CABDF61C3506F100005705 /* CGMBLEKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CGMBLEKit.h; sourceTree = "<group>"; };
 		43CABDF81C3506F100005705 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -137,9 +133,7 @@
 		7D9BF0EB2336EE80005DCFD6 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
 		7D9BF0F12336EE89005DCFD6 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
 		7D9BF11F2336FD7C005DCFD6 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
-		A9AD3821225EEEFB0058C179 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS5.1.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; };
 		A9D99F47225EE99300073DF6 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; };
-		B4D40D4623A42A1700D7ECB5 /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		E752B40F2063C31B0063027D /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
 		E75DB6AD20419B5D00FBE04E /* CalibrateGlucoseRxMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrateGlucoseRxMessage.swift; sourceTree = "<group>"; };
 		E76FD69B205C75780056DA5B /* Calibration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calibration.swift; sourceTree = "<group>"; };
@@ -161,13 +155,7 @@
 		437AFF172038EDF9008C4892 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
-				B4D40D4623A42A1700D7ECB5 /* LoopKit.framework */,
 				A9D99F47225EE99300073DF6 /* HealthKit.framework */,
-				A9AD3821225EEEFB0058C179 /* HealthKit.framework */,
-				43A8EC62210D4D1900A81379 /* LoopKit.framework */,
-				43A8EC49210D09BE00A81379 /* LoopKitUI.framework */,
-				43A8EC70210E629300A81379 /* ShareClient.framework */,
-				4325E9EE210EAF3F00969CE5 /* ShareClientUI.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";

+ 1 - 1
Dependencies/CGMBLEKit/CGMBLEKit/TransmitterManager.swift

@@ -178,7 +178,7 @@ public class TransmitterManager: TransmitterDelegate {
             return
         }
         
-        guard let quantity = glucose.glucose else {
+        guard glucose.glucose != nil else {
             delegate?.transmitterManager(self, didRead: [])
 //            updateDelegate(with: .noData)
             return

+ 2 - 2
Dependencies/CGMBLEKit/Common/HKUnit.swift

@@ -10,11 +10,11 @@ import HealthKit
 
 
 public extension HKUnit {
-    public static let milligramsPerDeciliter: HKUnit = {
+    static let milligramsPerDeciliter: HKUnit = {
         return HKUnit.gramUnit(with: .milli).unitDivided(by: HKUnit.literUnit(with: .deci))
     }()
 
-    public static let milligramsPerDeciliterPerMinute: HKUnit = {
+    static let milligramsPerDeciliterPerMinute: HKUnit = {
         return HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
     }()
 }

+ 388 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample.xcodeproj/project.pbxproj

@@ -0,0 +1,388 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 55;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		38567101272C4E39002C4DD8 /* LibreTramsmitterExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38567100272C4E39002C4DD8 /* LibreTramsmitterExampleApp.swift */; };
+		38567103272C4E39002C4DD8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38567102272C4E39002C4DD8 /* ContentView.swift */; };
+		38567105272C4E3B002C4DD8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38567104272C4E3B002C4DD8 /* Assets.xcassets */; };
+		38567108272C4E3B002C4DD8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38567107272C4E3B002C4DD8 /* Preview Assets.xcassets */; };
+		3856711B272C51A3002C4DD8 /* LibreTransmitter in Frameworks */ = {isa = PBXBuildFile; productRef = 3856711A272C51A3002C4DD8 /* LibreTransmitter */; };
+		38FEF403273AE2E300574A46 /* StateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF402273AE2E300574A46 /* StateModel.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		385670FD272C4E39002C4DD8 /* LibreTramsmitterExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LibreTramsmitterExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		38567100272C4E39002C4DD8 /* LibreTramsmitterExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTramsmitterExampleApp.swift; sourceTree = "<group>"; };
+		38567102272C4E39002C4DD8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+		38567104272C4E3B002C4DD8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		38567107272C4E3B002C4DD8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+		38567118272C5173002C4DD8 /* LibreTransmitter */ = {isa = PBXFileReference; lastKnownFileType = folder; name = LibreTransmitter; path = ..; sourceTree = "<group>"; };
+		3856711C272C5281002C4DD8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
+		38FEF3FF2739509400574A46 /* LibreTramsmitterExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LibreTramsmitterExample.entitlements; sourceTree = "<group>"; };
+		38FEF402273AE2E300574A46 /* StateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateModel.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		385670FA272C4E39002C4DD8 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				3856711B272C51A3002C4DD8 /* LibreTransmitter in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		3818AA43274BFE8A00843DB3 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		385670F4272C4E39002C4DD8 = {
+			isa = PBXGroup;
+			children = (
+				385670FF272C4E39002C4DD8 /* LibreTramsmitterExample */,
+				38567116272C5115002C4DD8 /* Packages */,
+				385670FE272C4E39002C4DD8 /* Products */,
+				3818AA43274BFE8A00843DB3 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		385670FE272C4E39002C4DD8 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				385670FD272C4E39002C4DD8 /* LibreTramsmitterExample.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		385670FF272C4E39002C4DD8 /* LibreTramsmitterExample */ = {
+			isa = PBXGroup;
+			children = (
+				38FEF3FF2739509400574A46 /* LibreTramsmitterExample.entitlements */,
+				3856711C272C5281002C4DD8 /* Info.plist */,
+				38567102272C4E39002C4DD8 /* ContentView.swift */,
+				38567100272C4E39002C4DD8 /* LibreTramsmitterExampleApp.swift */,
+				38567104272C4E3B002C4DD8 /* Assets.xcassets */,
+				38567106272C4E3B002C4DD8 /* Preview Content */,
+				38FEF402273AE2E300574A46 /* StateModel.swift */,
+			);
+			path = LibreTramsmitterExample;
+			sourceTree = "<group>";
+		};
+		38567106272C4E3B002C4DD8 /* Preview Content */ = {
+			isa = PBXGroup;
+			children = (
+				38567107272C4E3B002C4DD8 /* Preview Assets.xcassets */,
+			);
+			path = "Preview Content";
+			sourceTree = "<group>";
+		};
+		38567116272C5115002C4DD8 /* Packages */ = {
+			isa = PBXGroup;
+			children = (
+				38567118272C5173002C4DD8 /* LibreTransmitter */,
+			);
+			name = Packages;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		385670FC272C4E39002C4DD8 /* LibreTramsmitterExample */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 3856710B272C4E3B002C4DD8 /* Build configuration list for PBXNativeTarget "LibreTramsmitterExample" */;
+			buildPhases = (
+				385670F9272C4E39002C4DD8 /* Sources */,
+				385670FA272C4E39002C4DD8 /* Frameworks */,
+				385670FB272C4E39002C4DD8 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = LibreTramsmitterExample;
+			packageProductDependencies = (
+				3856711A272C51A3002C4DD8 /* LibreTransmitter */,
+			);
+			productName = LibreTramsmitterExample;
+			productReference = 385670FD272C4E39002C4DD8 /* LibreTramsmitterExample.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		385670F5272C4E39002C4DD8 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1310;
+				LastUpgradeCheck = 1310;
+				TargetAttributes = {
+					385670FC272C4E39002C4DD8 = {
+						CreatedOnToolsVersion = 13.1;
+					};
+				};
+			};
+			buildConfigurationList = 385670F8272C4E39002C4DD8 /* Build configuration list for PBXProject "LibreTramsmitterExample" */;
+			compatibilityVersion = "Xcode 13.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 385670F4272C4E39002C4DD8;
+			productRefGroup = 385670FE272C4E39002C4DD8 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				385670FC272C4E39002C4DD8 /* LibreTramsmitterExample */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		385670FB272C4E39002C4DD8 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				38567108272C4E3B002C4DD8 /* Preview Assets.xcassets in Resources */,
+				38567105272C4E3B002C4DD8 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		385670F9272C4E39002C4DD8 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				38FEF403273AE2E300574A46 /* StateModel.swift in Sources */,
+				38567103272C4E39002C4DD8 /* ContentView.swift in Sources */,
+				38567101272C4E39002C4DD8 /* LibreTramsmitterExampleApp.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		38567109272C4E3B002C4DD8 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		3856710A272C4E3B002C4DD8 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		3856710C272C4E3B002C4DD8 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = LibreTramsmitterExample/LibreTramsmitterExample.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "\"LibreTramsmitterExample/Preview Content\"";
+				DEVELOPMENT_TEAM = BA7ZHP4963;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = NO;
+				INFOPLIST_FILE = LibreTramsmitterExample/Info.plist;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.LibreTramsmitterExample;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		3856710D272C4E3B002C4DD8 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = LibreTramsmitterExample/LibreTramsmitterExample.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "\"LibreTramsmitterExample/Preview Content\"";
+				DEVELOPMENT_TEAM = BA7ZHP4963;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = NO;
+				INFOPLIST_FILE = LibreTramsmitterExample/Info.plist;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.LibreTramsmitterExample;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		385670F8272C4E39002C4DD8 /* Build configuration list for PBXProject "LibreTramsmitterExample" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				38567109272C4E3B002C4DD8 /* Debug */,
+				3856710A272C4E3B002C4DD8 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		3856710B272C4E3B002C4DD8 /* Build configuration list for PBXNativeTarget "LibreTramsmitterExample" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				3856710C272C4E3B002C4DD8 /* Debug */,
+				3856710D272C4E3B002C4DD8 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		3856711A272C51A3002C4DD8 /* LibreTransmitter */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = LibreTransmitter;
+		};
+/* End XCSwiftPackageProductDependency section */
+	};
+	rootObject = 385670F5272C4E39002C4DD8 /* Project object */;
+}

+ 7 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:">
+   </FileRef>
+</Workspace>

+ 8 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 11 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 98 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,98 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "60x60"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "60x60"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "76x76"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "76x76"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "83.5x83.5"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "scale" : "1x",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 66 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/ContentView.swift

@@ -0,0 +1,66 @@
+//
+//  ContentView.swift
+//  LibreTramsmitterExample
+//
+//  Created by Ivan Valkou on 29.10.2021.
+//
+
+import SwiftUI
+import LibreTransmitter
+import HealthKit
+
+struct ContentView: View {
+    @StateObject var state = StateModel()
+
+    @State var manager: LibreTransmitterManager? {
+        didSet {
+            manager?.cgmManagerDelegate = state
+        }
+    }
+
+    @State var setupPresented = false
+    @State var settingsPresented = false
+    @AppStorage("LibreTransmitterManager.configured") var configured = false
+
+    let unit = HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
+
+    var body: some View {
+        VStack(spacing: 50) {
+            Text("\(state.currentGlucose?.glucoseDouble ?? .nan)")
+            Text("\(state.trend.symbol)")
+
+            Button("Libre Transmitter") {
+                setupPresented = true
+            }
+
+            Button("Test alert sound") {
+                NotificationHelper.playSoundIfNeeded()
+            }
+        }
+        .sheet(isPresented: $setupPresented) {} content: {
+            if configured, let manager = manager {
+                LibreTransmitterSettingsView(
+                    manager: manager,
+                    glucoseUnit: unit) {
+                        self.manager = nil
+                        configured = false
+                    } completion: {
+                        setupPresented = false
+                    }
+
+            } else {
+                LibreTransmitterSetupView { manager in
+                    self.manager = manager
+                    configured = true
+                } completion: {
+                    setupPresented = false
+                }
+            }
+        }
+        .onAppear {
+            if configured {
+                manager = LibreTransmitterManager()
+            }
+        }
+    }
+}

+ 56 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Info.plist

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>NSBluetoothAlwaysUsageDescription</key>
+	<string>Because why not</string>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(MARKETING_VERSION)</string>
+	<key>CFBundleVersion</key>
+	<string>$(CURRENT_PROJECT_VERSION)</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UIApplicationSceneManifest</key>
+	<dict>
+		<key>UIApplicationSupportsMultipleScenes</key>
+		<true/>
+	</dict>
+	<key>UIApplicationSupportsIndirectInputEvents</key>
+	<true/>
+	<key>UIBackgroundModes</key>
+	<array>
+		<string>bluetooth-central</string>
+		<string>bluetooth-peripheral</string>
+	</array>
+	<key>UILaunchScreen</key>
+	<dict>
+		<key>UILaunchScreen</key>
+		<dict/>
+	</dict>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~iphone</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 8 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/LibreTramsmitterExample.entitlements

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.developer.usernotifications.time-sensitive</key>
+	<true/>
+</dict>
+</plist>

+ 17 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/LibreTramsmitterExampleApp.swift

@@ -0,0 +1,17 @@
+//
+//  LibreTramsmitterExampleApp.swift
+//  LibreTramsmitterExample
+//
+//  Created by Ivan Valkou on 29.10.2021.
+//
+
+import SwiftUI
+
+@main
+struct LibreTramsmitterExampleApp: App {
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+        }
+    }
+}

+ 6 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/Preview Content/Preview Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 43 - 0
Dependencies/LibreTransmitter/LibreTramsmitterExample/LibreTramsmitterExample/StateModel.swift

@@ -0,0 +1,43 @@
+//
+//  StateModel.swift
+//  LibreTramsmitterExample
+//
+//  Created by Ivan Valkou on 09.11.2021.
+//
+
+import SwiftUI
+import LibreTransmitter
+
+final class StateModel: ObservableObject {
+    private let delegateQueue = DispatchQueue(label: "StateModel.delegateQueue")
+    @Published var currentGlucose: LibreGlucose?
+    @Published var trend: GlucoseTrend = .flat
+}
+
+extension StateModel: LibreTransmitterManagerDelegate {
+    var queue: DispatchQueue {
+        delegateQueue
+    }
+
+    func startDateToFilterNewData(for: LibreTransmitterManager) -> Date? {
+        Date().addingTimeInterval(-3600)
+    }
+
+    func cgmManager(_ manager: LibreTransmitterManager, hasNew result: Result<[LibreGlucose], Error>) {
+        switch result {
+
+        case let .success(data):
+            print("New data: \(data)")
+            DispatchQueue.main.async {
+                self.trend = manager.glucoseDisplay?.trendType ?? .flat
+                self.currentGlucose = data.first
+            }
+        case let .failure(error):
+            print("Error: \(error.localizedDescription)")
+        }
+    }
+
+    func overcalibration(for: LibreTransmitterManager) -> ((Double) -> (Double))? {
+        nil
+    }
+}

+ 28 - 0
Dependencies/LibreTransmitter/Package.swift

@@ -0,0 +1,28 @@
+// swift-tools-version:5.5
+
+import PackageDescription
+
+let package = Package(
+    name: "LibreTransmitter",
+    platforms: [.iOS(.v14)],
+    products: [
+        .library(
+            name: "LibreTransmitter",
+            targets: ["LibreTransmitter"]),
+        .library(
+            name: "RawGlucose",
+            targets: ["RawGlucose"]),
+    ],
+    dependencies: [],
+    targets: [
+        .binaryTarget(
+            name: "RawGlucose",
+            path: "RawGlucose.xcframework"),
+        .target(
+            name: "LibreTransmitter",
+            dependencies: ["RawGlucose"],
+            resources: [.process("LibreTransmitterUI/Graphics")]
+        )
+
+    ]
+)

+ 3 - 0
Dependencies/LibreTransmitter/README.md

@@ -0,0 +1,3 @@
+# LibreTransmitter
+
+A description of this package.

+ 40 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/Info.plist

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>AvailableLibraries</key>
+	<array>
+		<dict>
+			<key>LibraryIdentifier</key>
+			<string>ios-arm64_x86_64-simulator</string>
+			<key>LibraryPath</key>
+			<string>RawGlucose.framework</string>
+			<key>SupportedArchitectures</key>
+			<array>
+				<string>arm64</string>
+				<string>x86_64</string>
+			</array>
+			<key>SupportedPlatform</key>
+			<string>ios</string>
+			<key>SupportedPlatformVariant</key>
+			<string>simulator</string>
+		</dict>
+		<dict>
+			<key>LibraryIdentifier</key>
+			<string>ios-arm64</string>
+			<key>LibraryPath</key>
+			<string>RawGlucose.framework</string>
+			<key>SupportedArchitectures</key>
+			<array>
+				<string>arm64</string>
+			</array>
+			<key>SupportedPlatform</key>
+			<string>ios</string>
+		</dict>
+	</array>
+	<key>CFBundlePackageType</key>
+	<string>XFWK</string>
+	<key>XCFrameworkFormatVersion</key>
+	<string>1.0</string>
+</dict>
+</plist>

+ 212 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Headers/RawGlucose-Swift.h

@@ -0,0 +1,212 @@
+// Generated by Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+#ifndef RAWGLUCOSE_SWIFT_H
+#define RAWGLUCOSE_SWIFT_H
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wgcc-compat"
+
+#if !defined(__has_include)
+# define __has_include(x) 0
+#endif
+#if !defined(__has_attribute)
+# define __has_attribute(x) 0
+#endif
+#if !defined(__has_feature)
+# define __has_feature(x) 0
+#endif
+#if !defined(__has_warning)
+# define __has_warning(x) 0
+#endif
+
+#if __has_include(<swift/objc-prologue.h>)
+# include <swift/objc-prologue.h>
+#endif
+
+#pragma clang diagnostic ignored "-Wauto-import"
+#include <Foundation/Foundation.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#if !defined(SWIFT_TYPEDEFS)
+# define SWIFT_TYPEDEFS 1
+# if __has_include(<uchar.h>)
+#  include <uchar.h>
+# elif !defined(__cplusplus)
+typedef uint_least16_t char16_t;
+typedef uint_least32_t char32_t;
+# endif
+typedef float swift_float2  __attribute__((__ext_vector_type__(2)));
+typedef float swift_float3  __attribute__((__ext_vector_type__(3)));
+typedef float swift_float4  __attribute__((__ext_vector_type__(4)));
+typedef double swift_double2  __attribute__((__ext_vector_type__(2)));
+typedef double swift_double3  __attribute__((__ext_vector_type__(3)));
+typedef double swift_double4  __attribute__((__ext_vector_type__(4)));
+typedef int swift_int2  __attribute__((__ext_vector_type__(2)));
+typedef int swift_int3  __attribute__((__ext_vector_type__(3)));
+typedef int swift_int4  __attribute__((__ext_vector_type__(4)));
+typedef unsigned int swift_uint2  __attribute__((__ext_vector_type__(2)));
+typedef unsigned int swift_uint3  __attribute__((__ext_vector_type__(3)));
+typedef unsigned int swift_uint4  __attribute__((__ext_vector_type__(4)));
+#endif
+
+#if !defined(SWIFT_PASTE)
+# define SWIFT_PASTE_HELPER(x, y) x##y
+# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
+#endif
+#if !defined(SWIFT_METATYPE)
+# define SWIFT_METATYPE(X) Class
+#endif
+#if !defined(SWIFT_CLASS_PROPERTY)
+# if __has_feature(objc_class_property)
+#  define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
+# else
+#  define SWIFT_CLASS_PROPERTY(...)
+# endif
+#endif
+
+#if __has_attribute(objc_runtime_name)
+# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
+#else
+# define SWIFT_RUNTIME_NAME(X)
+#endif
+#if __has_attribute(swift_name)
+# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
+#else
+# define SWIFT_COMPILE_NAME(X)
+#endif
+#if __has_attribute(objc_method_family)
+# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
+#else
+# define SWIFT_METHOD_FAMILY(X)
+#endif
+#if __has_attribute(noescape)
+# define SWIFT_NOESCAPE __attribute__((noescape))
+#else
+# define SWIFT_NOESCAPE
+#endif
+#if __has_attribute(ns_consumed)
+# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
+#else
+# define SWIFT_RELEASES_ARGUMENT
+#endif
+#if __has_attribute(warn_unused_result)
+# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+#else
+# define SWIFT_WARN_UNUSED_RESULT
+#endif
+#if __has_attribute(noreturn)
+# define SWIFT_NORETURN __attribute__((noreturn))
+#else
+# define SWIFT_NORETURN
+#endif
+#if !defined(SWIFT_CLASS_EXTRA)
+# define SWIFT_CLASS_EXTRA
+#endif
+#if !defined(SWIFT_PROTOCOL_EXTRA)
+# define SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_ENUM_EXTRA)
+# define SWIFT_ENUM_EXTRA
+#endif
+#if !defined(SWIFT_CLASS)
+# if __has_attribute(objc_subclassing_restricted)
+#  define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
+#  define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# else
+#  define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+#  define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# endif
+#endif
+#if !defined(SWIFT_RESILIENT_CLASS)
+# if __has_attribute(objc_class_stub)
+#  define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
+#  define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# else
+#  define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
+#  define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# endif
+#endif
+
+#if !defined(SWIFT_PROTOCOL)
+# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+#endif
+
+#if !defined(SWIFT_EXTENSION)
+# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
+#endif
+
+#if !defined(OBJC_DESIGNATED_INITIALIZER)
+# if __has_attribute(objc_designated_initializer)
+#  define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
+# else
+#  define OBJC_DESIGNATED_INITIALIZER
+# endif
+#endif
+#if !defined(SWIFT_ENUM_ATTR)
+# if defined(__has_attribute) && __has_attribute(enum_extensibility)
+#  define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
+# else
+#  define SWIFT_ENUM_ATTR(_extensibility)
+# endif
+#endif
+#if !defined(SWIFT_ENUM)
+# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# if __has_feature(generalized_swift_name)
+#  define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# else
+#  define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
+# endif
+#endif
+#if !defined(SWIFT_UNAVAILABLE)
+# define SWIFT_UNAVAILABLE __attribute__((unavailable))
+#endif
+#if !defined(SWIFT_UNAVAILABLE_MSG)
+# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
+#endif
+#if !defined(SWIFT_AVAILABILITY)
+# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
+#endif
+#if !defined(SWIFT_WEAK_IMPORT)
+# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
+#endif
+#if !defined(SWIFT_DEPRECATED)
+# define SWIFT_DEPRECATED __attribute__((deprecated))
+#endif
+#if !defined(SWIFT_DEPRECATED_MSG)
+# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
+#endif
+#if __has_feature(attribute_diagnose_if_objc)
+# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
+#else
+# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
+#endif
+#if !defined(IBSegueAction)
+# define IBSegueAction
+#endif
+#if __has_feature(modules)
+#if __has_warning("-Watimport-in-framework-header")
+#pragma clang diagnostic ignored "-Watimport-in-framework-header"
+#endif
+#endif
+
+#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
+#pragma clang diagnostic ignored "-Wduplicate-method-arg"
+#if __has_warning("-Wpragma-clang-attribute")
+# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
+#endif
+#pragma clang diagnostic ignored "-Wunknown-pragmas"
+#pragma clang diagnostic ignored "-Wnullability"
+
+#if __has_attribute(external_source_symbol)
+# pragma push_macro("any")
+# undef any
+# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="RawGlucose",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
+# pragma pop_macro("any")
+#endif
+
+#if __has_attribute(external_source_symbol)
+# pragma clang attribute pop
+#endif
+#pragma clang diagnostic pop
+#endif

+ 18 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Headers/RawGlucose.h

@@ -0,0 +1,18 @@
+//
+//  RawGlucose.h
+//  RawGlucose
+//
+//  Created by Ivan Valkou on 29.10.2021.
+//
+
+#import <Foundation/Foundation.h>
+
+//! Project version number for RawGlucose.
+FOUNDATION_EXPORT double RawGlucoseVersionNumber;
+
+//! Project version string for RawGlucose.
+FOUNDATION_EXPORT const unsigned char RawGlucoseVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import <RawGlucose/PublicHeader.h>
+
+

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Info.plist


BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftdoc


+ 8 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftinterface

@@ -0,0 +1,8 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+// swift-module-flags: -target arm64-apple-ios14.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name RawGlucose
+import Foundation
+@_exported import RawGlucose
+import Swift
+import _Concurrency
+public func glucoseValueFromRaw(rawTemperature: Swift.Double, rawTemperatureAdjustment: Swift.Double, rawGlucose: Swift.Double, i1: Swift.Int, i2: Swift.Int, i3: Swift.Double, i4: Swift.Double, i5: Swift.Double, i6: Swift.Double) -> Swift.Double

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftdoc


+ 8 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftinterface

@@ -0,0 +1,8 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+// swift-module-flags: -target arm64-apple-ios14.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name RawGlucose
+import Foundation
+@_exported import RawGlucose
+import Swift
+import _Concurrency
+public func glucoseValueFromRaw(rawTemperature: Swift.Double, rawTemperatureAdjustment: Swift.Double, rawGlucose: Swift.Double, i1: Swift.Int, i2: Swift.Int, i3: Swift.Double, i4: Swift.Double, i5: Swift.Double, i6: Swift.Double) -> Swift.Double

+ 11 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/Modules/module.modulemap

@@ -0,0 +1,11 @@
+framework module RawGlucose {
+  umbrella header "RawGlucose.h"
+
+  export *
+  module * { export * }
+}
+
+module RawGlucose.Swift {
+    header "RawGlucose-Swift.h"
+    requires objc
+}

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/RawGlucose


+ 201 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64/RawGlucose.framework/_CodeSignature/CodeResources

@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>files</key>
+	<dict>
+		<key>Headers/RawGlucose-Swift.h</key>
+		<data>
+		TbsdmPkVid1MmO08l/+IxleHiDQ=
+		</data>
+		<key>Headers/RawGlucose.h</key>
+		<data>
+		e5N6Xw/Q6Ssus1Sfka2OdAgEwfs=
+		</data>
+		<key>Info.plist</key>
+		<data>
+		tEul9y3almjw/PjiqgW3W7uKoME=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftdoc</key>
+		<data>
+		N3fVe2jpkhUblN1Mys8Q2Qhaa6w=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftinterface</key>
+		<data>
+		bDo7yq/Pn9WJy96YLVD14CFqy6k=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftmodule</key>
+		<data>
+		VJ11gvKIAReSPnvteZuJ/Zi2lzs=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftdoc</key>
+		<data>
+		N3fVe2jpkhUblN1Mys8Q2Qhaa6w=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftinterface</key>
+		<data>
+		bDo7yq/Pn9WJy96YLVD14CFqy6k=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftmodule</key>
+		<data>
+		VJ11gvKIAReSPnvteZuJ/Zi2lzs=
+		</data>
+		<key>Modules/module.modulemap</key>
+		<data>
+		Nbavz/+JmGFVpOq9rrggRbhjo/Y=
+		</data>
+	</dict>
+	<key>files2</key>
+	<dict>
+		<key>Headers/RawGlucose-Swift.h</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			QTI1cy5YKF3DvEw55SrCA5btCBewRFi+VYIPM8VFMxg=
+			</data>
+		</dict>
+		<key>Headers/RawGlucose.h</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			vWp1R9C9/5EBlkAhcUZ6DglPTPIL7LdzMYFDrxPPp2w=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftdoc</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			KD1y+BgwVATnQ7wQtEbcw5J5sG2J6FDDlUSo2Mc5vbk=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftinterface</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			zsjCd1qkk/OvTWr8aJ0L7SbzmnZA7/2pKuTHg3N9cgY=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios.swiftmodule</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			2Az19ev+8tlVF8RvybrcaMdD2qRfiAg2iEOK8NwNN0Q=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftdoc</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			KD1y+BgwVATnQ7wQtEbcw5J5sG2J6FDDlUSo2Mc5vbk=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftinterface</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			zsjCd1qkk/OvTWr8aJ0L7SbzmnZA7/2pKuTHg3N9cgY=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftmodule</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			2Az19ev+8tlVF8RvybrcaMdD2qRfiAg2iEOK8NwNN0Q=
+			</data>
+		</dict>
+		<key>Modules/module.modulemap</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			HGSrZF6T2tksU5dqClpOIcyQITvyjJOLdm6TEvIdU7M=
+			</data>
+		</dict>
+	</dict>
+	<key>rules</key>
+	<dict>
+		<key>^.*</key>
+		<true/>
+		<key>^.*\.lproj/</key>
+		<dict>
+			<key>optional</key>
+			<true/>
+			<key>weight</key>
+			<real>1000</real>
+		</dict>
+		<key>^.*\.lproj/locversion.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>1100</real>
+		</dict>
+		<key>^Base\.lproj/</key>
+		<dict>
+			<key>weight</key>
+			<real>1010</real>
+		</dict>
+		<key>^version.plist$</key>
+		<true/>
+	</dict>
+	<key>rules2</key>
+	<dict>
+		<key>.*\.dSYM($|/)</key>
+		<dict>
+			<key>weight</key>
+			<real>11</real>
+		</dict>
+		<key>^(.*/)?\.DS_Store$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>2000</real>
+		</dict>
+		<key>^.*</key>
+		<true/>
+		<key>^.*\.lproj/</key>
+		<dict>
+			<key>optional</key>
+			<true/>
+			<key>weight</key>
+			<real>1000</real>
+		</dict>
+		<key>^.*\.lproj/locversion.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>1100</real>
+		</dict>
+		<key>^Base\.lproj/</key>
+		<dict>
+			<key>weight</key>
+			<real>1010</real>
+		</dict>
+		<key>^Info\.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^PkgInfo$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^embedded\.provisionprofile$</key>
+		<dict>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^version\.plist$</key>
+		<dict>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 430 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Headers/RawGlucose-Swift.h

@@ -0,0 +1,430 @@
+#if 0
+#elif defined(__arm64__) && __arm64__
+// Generated by Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+#ifndef RAWGLUCOSE_SWIFT_H
+#define RAWGLUCOSE_SWIFT_H
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wgcc-compat"
+
+#if !defined(__has_include)
+# define __has_include(x) 0
+#endif
+#if !defined(__has_attribute)
+# define __has_attribute(x) 0
+#endif
+#if !defined(__has_feature)
+# define __has_feature(x) 0
+#endif
+#if !defined(__has_warning)
+# define __has_warning(x) 0
+#endif
+
+#if __has_include(<swift/objc-prologue.h>)
+# include <swift/objc-prologue.h>
+#endif
+
+#pragma clang diagnostic ignored "-Wauto-import"
+#include <Foundation/Foundation.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#if !defined(SWIFT_TYPEDEFS)
+# define SWIFT_TYPEDEFS 1
+# if __has_include(<uchar.h>)
+#  include <uchar.h>
+# elif !defined(__cplusplus)
+typedef uint_least16_t char16_t;
+typedef uint_least32_t char32_t;
+# endif
+typedef float swift_float2  __attribute__((__ext_vector_type__(2)));
+typedef float swift_float3  __attribute__((__ext_vector_type__(3)));
+typedef float swift_float4  __attribute__((__ext_vector_type__(4)));
+typedef double swift_double2  __attribute__((__ext_vector_type__(2)));
+typedef double swift_double3  __attribute__((__ext_vector_type__(3)));
+typedef double swift_double4  __attribute__((__ext_vector_type__(4)));
+typedef int swift_int2  __attribute__((__ext_vector_type__(2)));
+typedef int swift_int3  __attribute__((__ext_vector_type__(3)));
+typedef int swift_int4  __attribute__((__ext_vector_type__(4)));
+typedef unsigned int swift_uint2  __attribute__((__ext_vector_type__(2)));
+typedef unsigned int swift_uint3  __attribute__((__ext_vector_type__(3)));
+typedef unsigned int swift_uint4  __attribute__((__ext_vector_type__(4)));
+#endif
+
+#if !defined(SWIFT_PASTE)
+# define SWIFT_PASTE_HELPER(x, y) x##y
+# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
+#endif
+#if !defined(SWIFT_METATYPE)
+# define SWIFT_METATYPE(X) Class
+#endif
+#if !defined(SWIFT_CLASS_PROPERTY)
+# if __has_feature(objc_class_property)
+#  define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
+# else
+#  define SWIFT_CLASS_PROPERTY(...)
+# endif
+#endif
+
+#if __has_attribute(objc_runtime_name)
+# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
+#else
+# define SWIFT_RUNTIME_NAME(X)
+#endif
+#if __has_attribute(swift_name)
+# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
+#else
+# define SWIFT_COMPILE_NAME(X)
+#endif
+#if __has_attribute(objc_method_family)
+# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
+#else
+# define SWIFT_METHOD_FAMILY(X)
+#endif
+#if __has_attribute(noescape)
+# define SWIFT_NOESCAPE __attribute__((noescape))
+#else
+# define SWIFT_NOESCAPE
+#endif
+#if __has_attribute(ns_consumed)
+# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
+#else
+# define SWIFT_RELEASES_ARGUMENT
+#endif
+#if __has_attribute(warn_unused_result)
+# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+#else
+# define SWIFT_WARN_UNUSED_RESULT
+#endif
+#if __has_attribute(noreturn)
+# define SWIFT_NORETURN __attribute__((noreturn))
+#else
+# define SWIFT_NORETURN
+#endif
+#if !defined(SWIFT_CLASS_EXTRA)
+# define SWIFT_CLASS_EXTRA
+#endif
+#if !defined(SWIFT_PROTOCOL_EXTRA)
+# define SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_ENUM_EXTRA)
+# define SWIFT_ENUM_EXTRA
+#endif
+#if !defined(SWIFT_CLASS)
+# if __has_attribute(objc_subclassing_restricted)
+#  define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
+#  define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# else
+#  define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+#  define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# endif
+#endif
+#if !defined(SWIFT_RESILIENT_CLASS)
+# if __has_attribute(objc_class_stub)
+#  define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
+#  define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# else
+#  define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
+#  define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# endif
+#endif
+
+#if !defined(SWIFT_PROTOCOL)
+# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+#endif
+
+#if !defined(SWIFT_EXTENSION)
+# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
+#endif
+
+#if !defined(OBJC_DESIGNATED_INITIALIZER)
+# if __has_attribute(objc_designated_initializer)
+#  define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
+# else
+#  define OBJC_DESIGNATED_INITIALIZER
+# endif
+#endif
+#if !defined(SWIFT_ENUM_ATTR)
+# if defined(__has_attribute) && __has_attribute(enum_extensibility)
+#  define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
+# else
+#  define SWIFT_ENUM_ATTR(_extensibility)
+# endif
+#endif
+#if !defined(SWIFT_ENUM)
+# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# if __has_feature(generalized_swift_name)
+#  define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# else
+#  define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
+# endif
+#endif
+#if !defined(SWIFT_UNAVAILABLE)
+# define SWIFT_UNAVAILABLE __attribute__((unavailable))
+#endif
+#if !defined(SWIFT_UNAVAILABLE_MSG)
+# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
+#endif
+#if !defined(SWIFT_AVAILABILITY)
+# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
+#endif
+#if !defined(SWIFT_WEAK_IMPORT)
+# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
+#endif
+#if !defined(SWIFT_DEPRECATED)
+# define SWIFT_DEPRECATED __attribute__((deprecated))
+#endif
+#if !defined(SWIFT_DEPRECATED_MSG)
+# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
+#endif
+#if __has_feature(attribute_diagnose_if_objc)
+# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
+#else
+# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
+#endif
+#if !defined(IBSegueAction)
+# define IBSegueAction
+#endif
+#if __has_feature(modules)
+#if __has_warning("-Watimport-in-framework-header")
+#pragma clang diagnostic ignored "-Watimport-in-framework-header"
+#endif
+#endif
+
+#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
+#pragma clang diagnostic ignored "-Wduplicate-method-arg"
+#if __has_warning("-Wpragma-clang-attribute")
+# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
+#endif
+#pragma clang diagnostic ignored "-Wunknown-pragmas"
+#pragma clang diagnostic ignored "-Wnullability"
+
+#if __has_attribute(external_source_symbol)
+# pragma push_macro("any")
+# undef any
+# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="RawGlucose",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
+# pragma pop_macro("any")
+#endif
+
+#if __has_attribute(external_source_symbol)
+# pragma clang attribute pop
+#endif
+#pragma clang diagnostic pop
+#endif
+
+#elif defined(__x86_64__) && __x86_64__
+// Generated by Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+#ifndef RAWGLUCOSE_SWIFT_H
+#define RAWGLUCOSE_SWIFT_H
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wgcc-compat"
+
+#if !defined(__has_include)
+# define __has_include(x) 0
+#endif
+#if !defined(__has_attribute)
+# define __has_attribute(x) 0
+#endif
+#if !defined(__has_feature)
+# define __has_feature(x) 0
+#endif
+#if !defined(__has_warning)
+# define __has_warning(x) 0
+#endif
+
+#if __has_include(<swift/objc-prologue.h>)
+# include <swift/objc-prologue.h>
+#endif
+
+#pragma clang diagnostic ignored "-Wauto-import"
+#include <Foundation/Foundation.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#if !defined(SWIFT_TYPEDEFS)
+# define SWIFT_TYPEDEFS 1
+# if __has_include(<uchar.h>)
+#  include <uchar.h>
+# elif !defined(__cplusplus)
+typedef uint_least16_t char16_t;
+typedef uint_least32_t char32_t;
+# endif
+typedef float swift_float2  __attribute__((__ext_vector_type__(2)));
+typedef float swift_float3  __attribute__((__ext_vector_type__(3)));
+typedef float swift_float4  __attribute__((__ext_vector_type__(4)));
+typedef double swift_double2  __attribute__((__ext_vector_type__(2)));
+typedef double swift_double3  __attribute__((__ext_vector_type__(3)));
+typedef double swift_double4  __attribute__((__ext_vector_type__(4)));
+typedef int swift_int2  __attribute__((__ext_vector_type__(2)));
+typedef int swift_int3  __attribute__((__ext_vector_type__(3)));
+typedef int swift_int4  __attribute__((__ext_vector_type__(4)));
+typedef unsigned int swift_uint2  __attribute__((__ext_vector_type__(2)));
+typedef unsigned int swift_uint3  __attribute__((__ext_vector_type__(3)));
+typedef unsigned int swift_uint4  __attribute__((__ext_vector_type__(4)));
+#endif
+
+#if !defined(SWIFT_PASTE)
+# define SWIFT_PASTE_HELPER(x, y) x##y
+# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
+#endif
+#if !defined(SWIFT_METATYPE)
+# define SWIFT_METATYPE(X) Class
+#endif
+#if !defined(SWIFT_CLASS_PROPERTY)
+# if __has_feature(objc_class_property)
+#  define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
+# else
+#  define SWIFT_CLASS_PROPERTY(...)
+# endif
+#endif
+
+#if __has_attribute(objc_runtime_name)
+# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
+#else
+# define SWIFT_RUNTIME_NAME(X)
+#endif
+#if __has_attribute(swift_name)
+# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
+#else
+# define SWIFT_COMPILE_NAME(X)
+#endif
+#if __has_attribute(objc_method_family)
+# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
+#else
+# define SWIFT_METHOD_FAMILY(X)
+#endif
+#if __has_attribute(noescape)
+# define SWIFT_NOESCAPE __attribute__((noescape))
+#else
+# define SWIFT_NOESCAPE
+#endif
+#if __has_attribute(ns_consumed)
+# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
+#else
+# define SWIFT_RELEASES_ARGUMENT
+#endif
+#if __has_attribute(warn_unused_result)
+# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+#else
+# define SWIFT_WARN_UNUSED_RESULT
+#endif
+#if __has_attribute(noreturn)
+# define SWIFT_NORETURN __attribute__((noreturn))
+#else
+# define SWIFT_NORETURN
+#endif
+#if !defined(SWIFT_CLASS_EXTRA)
+# define SWIFT_CLASS_EXTRA
+#endif
+#if !defined(SWIFT_PROTOCOL_EXTRA)
+# define SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_ENUM_EXTRA)
+# define SWIFT_ENUM_EXTRA
+#endif
+#if !defined(SWIFT_CLASS)
+# if __has_attribute(objc_subclassing_restricted)
+#  define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
+#  define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# else
+#  define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+#  define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# endif
+#endif
+#if !defined(SWIFT_RESILIENT_CLASS)
+# if __has_attribute(objc_class_stub)
+#  define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
+#  define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# else
+#  define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
+#  define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# endif
+#endif
+
+#if !defined(SWIFT_PROTOCOL)
+# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+#endif
+
+#if !defined(SWIFT_EXTENSION)
+# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
+#endif
+
+#if !defined(OBJC_DESIGNATED_INITIALIZER)
+# if __has_attribute(objc_designated_initializer)
+#  define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
+# else
+#  define OBJC_DESIGNATED_INITIALIZER
+# endif
+#endif
+#if !defined(SWIFT_ENUM_ATTR)
+# if defined(__has_attribute) && __has_attribute(enum_extensibility)
+#  define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
+# else
+#  define SWIFT_ENUM_ATTR(_extensibility)
+# endif
+#endif
+#if !defined(SWIFT_ENUM)
+# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# if __has_feature(generalized_swift_name)
+#  define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# else
+#  define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
+# endif
+#endif
+#if !defined(SWIFT_UNAVAILABLE)
+# define SWIFT_UNAVAILABLE __attribute__((unavailable))
+#endif
+#if !defined(SWIFT_UNAVAILABLE_MSG)
+# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
+#endif
+#if !defined(SWIFT_AVAILABILITY)
+# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
+#endif
+#if !defined(SWIFT_WEAK_IMPORT)
+# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
+#endif
+#if !defined(SWIFT_DEPRECATED)
+# define SWIFT_DEPRECATED __attribute__((deprecated))
+#endif
+#if !defined(SWIFT_DEPRECATED_MSG)
+# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
+#endif
+#if __has_feature(attribute_diagnose_if_objc)
+# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
+#else
+# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
+#endif
+#if !defined(IBSegueAction)
+# define IBSegueAction
+#endif
+#if __has_feature(modules)
+#if __has_warning("-Watimport-in-framework-header")
+#pragma clang diagnostic ignored "-Watimport-in-framework-header"
+#endif
+#endif
+
+#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
+#pragma clang diagnostic ignored "-Wduplicate-method-arg"
+#if __has_warning("-Wpragma-clang-attribute")
+# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
+#endif
+#pragma clang diagnostic ignored "-Wunknown-pragmas"
+#pragma clang diagnostic ignored "-Wnullability"
+
+#if __has_attribute(external_source_symbol)
+# pragma push_macro("any")
+# undef any
+# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="RawGlucose",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
+# pragma pop_macro("any")
+#endif
+
+#if __has_attribute(external_source_symbol)
+# pragma clang attribute pop
+#endif
+#pragma clang diagnostic pop
+#endif
+
+#endif

+ 18 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Headers/RawGlucose.h

@@ -0,0 +1,18 @@
+//
+//  RawGlucose.h
+//  RawGlucose
+//
+//  Created by Ivan Valkou on 29.10.2021.
+//
+
+#import <Foundation/Foundation.h>
+
+//! Project version number for RawGlucose.
+FOUNDATION_EXPORT double RawGlucoseVersionNumber;
+
+//! Project version string for RawGlucose.
+FOUNDATION_EXPORT const unsigned char RawGlucoseVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import <RawGlucose/PublicHeader.h>
+
+

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Info.plist


BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftdoc


+ 8 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftinterface

@@ -0,0 +1,8 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+// swift-module-flags: -target arm64-apple-ios14.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name RawGlucose
+import Foundation
+@_exported import RawGlucose
+import Swift
+import _Concurrency
+public func glucoseValueFromRaw(rawTemperature: Swift.Double, rawTemperatureAdjustment: Swift.Double, rawGlucose: Swift.Double, i1: Swift.Int, i2: Swift.Int, i3: Swift.Double, i4: Swift.Double, i5: Swift.Double, i6: Swift.Double) -> Swift.Double

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftdoc


+ 8 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/arm64.swiftinterface

@@ -0,0 +1,8 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+// swift-module-flags: -target arm64-apple-ios14.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name RawGlucose
+import Foundation
+@_exported import RawGlucose
+import Swift
+import _Concurrency
+public func glucoseValueFromRaw(rawTemperature: Swift.Double, rawTemperatureAdjustment: Swift.Double, rawGlucose: Swift.Double, i1: Swift.Int, i2: Swift.Int, i3: Swift.Double, i4: Swift.Double, i5: Swift.Double, i6: Swift.Double) -> Swift.Double

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftdoc


+ 8 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftinterface

@@ -0,0 +1,8 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+// swift-module-flags: -target x86_64-apple-ios14.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name RawGlucose
+import Foundation
+@_exported import RawGlucose
+import Swift
+import _Concurrency
+public func glucoseValueFromRaw(rawTemperature: Swift.Double, rawTemperatureAdjustment: Swift.Double, rawGlucose: Swift.Double, i1: Swift.Int, i2: Swift.Int, i3: Swift.Double, i4: Swift.Double, i5: Swift.Double, i6: Swift.Double) -> Swift.Double

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64.swiftdoc


+ 8 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/RawGlucose.swiftmodule/x86_64.swiftinterface

@@ -0,0 +1,8 @@
+// swift-interface-format-version: 1.0
+// swift-compiler-version: Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6)
+// swift-module-flags: -target x86_64-apple-ios14.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name RawGlucose
+import Foundation
+@_exported import RawGlucose
+import Swift
+import _Concurrency
+public func glucoseValueFromRaw(rawTemperature: Swift.Double, rawTemperatureAdjustment: Swift.Double, rawGlucose: Swift.Double, i1: Swift.Int, i2: Swift.Int, i3: Swift.Double, i4: Swift.Double, i5: Swift.Double, i6: Swift.Double) -> Swift.Double

+ 11 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/Modules/module.modulemap

@@ -0,0 +1,11 @@
+framework module RawGlucose {
+  umbrella header "RawGlucose.h"
+
+  export *
+  module * { export * }
+}
+
+module RawGlucose.Swift {
+    header "RawGlucose-Swift.h"
+    requires objc
+}

BIN
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/RawGlucose


+ 267 - 0
Dependencies/LibreTransmitter/RawGlucose.xcframework/ios-arm64_x86_64-simulator/RawGlucose.framework/_CodeSignature/CodeResources

@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>files</key>
+	<dict>
+		<key>Headers/RawGlucose-Swift.h</key>
+		<data>
+		NOPVTsnUQdvUXiVvlGkcrHBsYf8=
+		</data>
+		<key>Headers/RawGlucose.h</key>
+		<data>
+		e5N6Xw/Q6Ssus1Sfka2OdAgEwfs=
+		</data>
+		<key>Info.plist</key>
+		<data>
+		C822WpDoMUZHHWfCJ8tfMtklvCc=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
+		<data>
+		XVOH12u7W5mUldmztR8JA2sGUBE=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
+		<data>
+		3G/qIi+knDm6DNBZMdut0+I0F8U=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
+		<data>
+		8XbBt5zfUNlmyrYv31HsRH5I7Pc=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftdoc</key>
+		<data>
+		XVOH12u7W5mUldmztR8JA2sGUBE=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftinterface</key>
+		<data>
+		3G/qIi+knDm6DNBZMdut0+I0F8U=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftmodule</key>
+		<data>
+		8XbBt5zfUNlmyrYv31HsRH5I7Pc=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
+		<data>
+		YTLCQrGaBLjG2V3HCaucJ1Affak=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
+		<data>
+		8+AhO+0B1RfBn7TTtbJhId3or3k=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
+		<data>
+		fBhjoSFy5wsrXWVEqv2bue3Tdho=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/x86_64.swiftdoc</key>
+		<data>
+		YTLCQrGaBLjG2V3HCaucJ1Affak=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/x86_64.swiftinterface</key>
+		<data>
+		8+AhO+0B1RfBn7TTtbJhId3or3k=
+		</data>
+		<key>Modules/RawGlucose.swiftmodule/x86_64.swiftmodule</key>
+		<data>
+		fBhjoSFy5wsrXWVEqv2bue3Tdho=
+		</data>
+		<key>Modules/module.modulemap</key>
+		<data>
+		Nbavz/+JmGFVpOq9rrggRbhjo/Y=
+		</data>
+	</dict>
+	<key>files2</key>
+	<dict>
+		<key>Headers/RawGlucose-Swift.h</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			05/Gxrpmj85ok9eADvswbwYSHS2GDU+dONqnxWX0YKc=
+			</data>
+		</dict>
+		<key>Headers/RawGlucose.h</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			vWp1R9C9/5EBlkAhcUZ6DglPTPIL7LdzMYFDrxPPp2w=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftdoc</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			R0b1kXmBK9IQpw2/AgSdiCUB3tyXVOLVYFhrcMiJTdc=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftinterface</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			JIy4JCrP2htiXMPFm2mtQWks0BAt8xJ4N8zKSbVi4WU=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64-apple-ios-simulator.swiftmodule</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			WvvLbXOvP/MljSINhZaIT3RXsDVf/dFvySSwZ2qgElg=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftdoc</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			R0b1kXmBK9IQpw2/AgSdiCUB3tyXVOLVYFhrcMiJTdc=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftinterface</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			JIy4JCrP2htiXMPFm2mtQWks0BAt8xJ4N8zKSbVi4WU=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/arm64.swiftmodule</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			WvvLbXOvP/MljSINhZaIT3RXsDVf/dFvySSwZ2qgElg=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftdoc</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			nLt/uTqHb2O8ehdLv6Pykcy9VYdbwfpAEJ7j8At6ruU=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftinterface</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			odDK6zWxPbRZLxn8UV8Pb3KyYW50utHNICmLKU3+B3I=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/x86_64-apple-ios-simulator.swiftmodule</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			QEolRuoZA6Gqn9VoRQId+9TRRhZAgZwIQgjHYACF9wk=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/x86_64.swiftdoc</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			nLt/uTqHb2O8ehdLv6Pykcy9VYdbwfpAEJ7j8At6ruU=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/x86_64.swiftinterface</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			odDK6zWxPbRZLxn8UV8Pb3KyYW50utHNICmLKU3+B3I=
+			</data>
+		</dict>
+		<key>Modules/RawGlucose.swiftmodule/x86_64.swiftmodule</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			QEolRuoZA6Gqn9VoRQId+9TRRhZAgZwIQgjHYACF9wk=
+			</data>
+		</dict>
+		<key>Modules/module.modulemap</key>
+		<dict>
+			<key>hash2</key>
+			<data>
+			HGSrZF6T2tksU5dqClpOIcyQITvyjJOLdm6TEvIdU7M=
+			</data>
+		</dict>
+	</dict>
+	<key>rules</key>
+	<dict>
+		<key>^.*</key>
+		<true/>
+		<key>^.*\.lproj/</key>
+		<dict>
+			<key>optional</key>
+			<true/>
+			<key>weight</key>
+			<real>1000</real>
+		</dict>
+		<key>^.*\.lproj/locversion.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>1100</real>
+		</dict>
+		<key>^Base\.lproj/</key>
+		<dict>
+			<key>weight</key>
+			<real>1010</real>
+		</dict>
+		<key>^version.plist$</key>
+		<true/>
+	</dict>
+	<key>rules2</key>
+	<dict>
+		<key>.*\.dSYM($|/)</key>
+		<dict>
+			<key>weight</key>
+			<real>11</real>
+		</dict>
+		<key>^(.*/)?\.DS_Store$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>2000</real>
+		</dict>
+		<key>^.*</key>
+		<true/>
+		<key>^.*\.lproj/</key>
+		<dict>
+			<key>optional</key>
+			<true/>
+			<key>weight</key>
+			<real>1000</real>
+		</dict>
+		<key>^.*\.lproj/locversion.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>1100</real>
+		</dict>
+		<key>^Base\.lproj/</key>
+		<dict>
+			<key>weight</key>
+			<real>1010</real>
+		</dict>
+		<key>^Info\.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^PkgInfo$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^embedded\.provisionprofile$</key>
+		<dict>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^version\.plist$</key>
+		<dict>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 258 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/BluetoothSearch.swift

@@ -0,0 +1,258 @@
+//
+//  BluetoothSearch.swift
+//  MiaomiaoClientUI
+//
+//  Created by Bjørn Inge Berg on 26/07/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import CoreBluetooth
+import Foundation
+import OSLog
+import UIKit
+import Combine
+
+struct RSSIInfo {
+    let bledeviceID:  String
+    let signalStrength: Int
+
+    var totalBars : Int {
+        3
+    }
+
+    var signalBars : Int {
+        if signalStrength < -80 {
+            return 1  //near
+        }
+
+        if signalStrength > -50 {
+            return 3 //immediate
+        }
+
+        return 2 //near
+    }
+
+
+
+}
+
+public final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
+
+    var centralManager: CBCentralManager!
+
+    fileprivate lazy var logger = Logger(forType: Self.self)
+
+    //fileprivate let deviceNames = SupportedDevices.allNames
+    //fileprivate let serviceUUIDs:[CBUUID]? = [CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")]
+
+    private var discoveredDevices = [CBPeripheral]()
+
+    public let passThrough = PassthroughSubject<CBPeripheral, Never>()
+    public let passThroughMetaData = PassthroughSubject<(CBPeripheral, [String: Any]), Never>()
+    let throttledRSSI = GenericThrottler(identificator: \RSSIInfo.bledeviceID, interval: 5)
+
+
+    private var rescanTimerBag = Set<AnyCancellable>()
+
+    public func addDiscoveredDevice(_ device: CBPeripheral, with metadata: [String: Any], rssi: Int) {
+        passThrough.send(device)
+        passThroughMetaData.send((device, metadata))
+        throttledRSSI.incoming.send(RSSIInfo(bledeviceID: device.identifier.uuidString, signalStrength: rssi))
+    }
+
+    public override init() {
+        super.init()
+        // calling readrssi on a peripheral is only supported on connected peripherals
+        // here we want the AllowDuplicatesKey to be true so that we get a continous feed of new rssi values for
+        // discovered but unconnected peripherals
+        // This should work, but in practice, most devices will still only be discovered once, meaning that we cannot update rssi values
+        // without either a new scan, or connecting to the peripheral and using .readrssi()
+        //centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
+        centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
+        //        slipBuffer.delegate = self
+        logger.debug("BluetoothSearchManager init called ")
+
+        // Ugly hack to be able to update rssi continously without connecting to peripheral
+        // Yes, this consumes extra power, but this feature is very convenient when needed, but very rarely used (only during setup)
+        //startTimer()
+    }
+
+    public func startTimer(){
+        stopTimer()
+
+        Timer.publish(every: 10, on: .main, in: .default)
+        .autoconnect()
+        .sink(
+            receiveValue: { [weak self ] _ in
+                self?.rescan()
+            }
+        )
+        .store(in: &rescanTimerBag)
+    }
+
+    public func stopTimer() {
+        if !rescanTimerBag.isEmpty {
+            rescanTimerBag.forEach { cancel in
+                cancel.cancel()
+            }
+        }
+    }
+
+    func rescan() {
+        if centralManager.isScanning {
+            centralManager.stopScan()
+        }
+        logger.debug("Rescanning")
+
+        self.scanForCompatibleDevices()
+    }
+
+    func scanForCompatibleDevices() {
+
+        
+
+        if centralManager.state == .poweredOn && !centralManager.isScanning {
+            logger.debug("Before scan for transmitter while central manager state \(String(describing: self.centralManager.state.rawValue))")
+
+            let allServices : [CBUUID] = LibreTransmitters.all.compactMap { atype in
+                atype.serviceUUID.first?.value
+            }
+
+
+            logger.debug("Will scan for the following services: \(String(describing: allServices))")
+
+
+            // nil because miamiao1 not advertising it's services
+            centralManager.scanForPeripherals(withServices: nil, options:nil)
+
+
+            // Ugly hack to be able to update rssi continously without connecting to peripheral
+            // Yes, this consumes extra power, but this feature is very convenient when needed, but very rarely used (only during setup)
+            startTimer()
+        }
+    }
+
+    func disconnectManually() {
+        logger.debug("did disconnect manually")
+        //        NotificationManager.scheduleDebugNotification(message: "Timer fired in Background", wait: 3)
+        //        _ = Timer(timeInterval: 150, repeats: false, block: {timer in NotificationManager.scheduleDebugNotification(message: "Timer fired in Background", wait: 0.5)})
+
+            if centralManager.isScanning {
+                centralManager.stopScan()
+            }
+    }
+
+
+
+    // MARK: - CBCentralManagerDelegate
+
+    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
+        logger.debug("Central Manager did update state to \(String(describing: central.state.rawValue))")
+        switch central.state {
+        case .poweredOff, .resetting, .unauthorized, .unknown, .unsupported:
+            logger.debug("Central Manager was either .poweredOff, .resetting, .unauthorized, .unknown, .unsupported: \(String(describing: central.state))")
+        case .poweredOn:
+            //we don't want this to start scanning right away, but rather wait until the view has appeared
+            // this means that the view is responsible for calling scanForCompatibleDevices it self
+            //scanForCompatibleDevices() // power was switched on, while app is running -> reconnect.
+            break
+
+        @unknown default:
+            fatalError("libre bluetooth state unhandled")
+        }
+    }
+
+    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
+        guard let name = peripheral.name?.lowercased() else {
+            logger.debug("dabear:: could not find name for device \(peripheral.identifier.uuidString)")
+            return
+        }
+
+        if LibreTransmitters.isSupported(peripheral) {
+            logger.debug("dabear:: did recognize device: \(name): \(peripheral.identifier)")
+            self.addDiscoveredDevice(peripheral, with: advertisementData, rssi: RSSI.intValue)
+            //peripheral.delegate = self
+            //peripheral.readRSSI()
+        } else {
+            logger.debug("dabear:: did not add unknown device: \(name): \(peripheral.identifier)")
+        }
+    }
+
+    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
+        //self.lastConnectedIdentifier = peripheral.identifier.uuidString
+
+    }
+
+    public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
+        logger.error("did fail to connect")
+    }
+
+    public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
+        logger.debug("did didDisconnectPeripheral")
+    }
+
+    // MARK: - CBPeripheralDelegate
+
+    public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
+        logger.debug("Did discover services")
+        if let error = error {
+            logger.error("Did discover services error: \(error.localizedDescription)")
+        }
+
+        if let services = peripheral.services {
+            for service in services {
+                peripheral.discoverCharacteristics(nil, for: service)
+
+                logger.debug("Did discover service: \(String(describing: service.debugDescription))")
+            }
+        }
+    }
+
+    public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
+        logger.debug("Did discover characteristics for service \(String(describing: peripheral.name))")
+
+        if let error = error {
+            logger.error("Did discover characteristics for service error: \(error.localizedDescription)")
+        }
+
+        if let characteristics = service.characteristics {
+            for characteristic in characteristics {
+                logger.debug("Did discover characteristic: \(String(describing: characteristic.debugDescription))")
+
+                if (characteristic.properties.intersection(.notify)) == .notify && characteristic.uuid == CBUUID(string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E") {
+                    peripheral.setNotifyValue(true, for: characteristic)
+                    logger.debug("Set notify value for this characteristic")
+                }
+                if characteristic.uuid == CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E") {
+                    //writeCharacteristic = characteristic
+                }
+            }
+        } else {
+            logger.error("Discovered characteristics, but no characteristics listed. There must be some error.")
+        }
+    }
+
+    
+    public func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
+
+        //throttledRSSI.incoming.send(RSSIInfo(bledeviceID: peripheral.identifier.uuidString, signalStrength: RSSI.intValue))
+
+        //peripheral.readRSSI() //we keep contuing to update the rssi (only works if peripheral is already connected....
+
+    }
+    public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
+        logger.debug("Did update notification state for characteristic: \(String(describing: characteristic.debugDescription))")
+    }
+
+    public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
+        logger.debug("Did update value for characteristic: \(String(describing: characteristic.debugDescription))")
+    }
+
+    public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
+        logger.debug("Did Write value \(String(characteristic.value.debugDescription)) for characteristic \(String(characteristic.debugDescription))")
+    }
+
+    deinit {
+        logger.debug("dabear:: BluetoothSearchManager deinit called")
+    }
+}

+ 84 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/CBPeripheralExtensions.swift

@@ -0,0 +1,84 @@
+//
+//  CBPeripheralExtensions.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 19/10/2020.
+//  Copyright © 2020 Bjørn Inge Vikhammermo Berg. All rights reserved.
+//
+
+import CoreBluetooth
+import Foundation
+
+public protocol PeripheralProtocol {
+    var name: String? { get }
+    var name2: String { get }
+
+    var asStringIdentifier: String { get }
+}
+
+public enum Either<A, B> {
+  case Left(A)
+  case Right(B)
+}
+
+public typealias SomePeripheral = Either<CBPeripheral, MockedPeripheral>
+
+extension SomePeripheral: PeripheralProtocol, Identifiable, Hashable, Equatable {
+    public static func == (lhs: Either<A, B>, rhs: Either<A, B>) -> Bool {
+        lhs.asStringIdentifier == rhs.asStringIdentifier
+    }
+
+    public var id: String {
+        actualPeripheral.asStringIdentifier
+    }
+
+    public func hash(into hasher: inout Hasher) {
+        hasher.combine(actualPeripheral.asStringIdentifier)
+    }
+
+    private var actualPeripheral: PeripheralProtocol {
+        switch self {
+        case let .Left(real):
+            return real
+        case let .Right(mocked):
+            return mocked
+        }
+    }
+    public var name: String? {
+        actualPeripheral.name
+    }
+
+    public var name2: String {
+        actualPeripheral.name2
+    }
+
+    public var asStringIdentifier: String {
+        actualPeripheral.asStringIdentifier
+    }
+}
+
+extension CBPeripheral: PeripheralProtocol, Identifiable {
+    public var name2: String {
+        self.name ?? ""
+    }
+
+    public var asStringIdentifier: String {
+        self.identifier.uuidString
+    }
+}
+
+public class MockedPeripheral: PeripheralProtocol, Identifiable {
+    public var name: String?
+
+    public var name2: String {
+        name ?? "unknown-device"
+    }
+
+    public var asStringIdentifier: String {
+        name2
+    }
+
+    public init(name: String) {
+        self.name = name
+    }
+}

+ 111 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/GenericThrottler.swift

@@ -0,0 +1,111 @@
+//
+//  GenericThrottler.swift
+//  LibreTransmitter
+//
+//  Created by Bjørn Inge Berg on 16/08/2021.
+//  Copyright © 2021 Mark Wilson. All rights reserved.
+//
+
+import Foundation
+import Combine
+
+class GenericThrottler<T, U: Hashable>{
+
+    public var throttledPublisher : AnyPublisher<T, Never> {
+        throttledSubject.eraseToAnyPublisher()
+    }
+    //this is the where the bluetoothsearch should send its updates
+    public let incoming = PassthroughSubject<T, Never>()
+
+    //this is what swiftui would connect to
+    private let throttledSubject = PassthroughSubject<T, Never>()
+    private var initiallyPublished = Set<U>()
+
+    private var bag = Set<AnyCancellable>()
+    private var timerBag = Set<AnyCancellable>()
+
+
+    private var newValues : [U: T] = [:]
+
+    private var identificator : KeyPath<T, U>
+
+    private var interval: TimeInterval
+
+    public func startTimer(){
+        stopTimer()
+
+        Timer.publish(every: interval, on: .main, in: .default)
+        .autoconnect()
+        .sink(
+            receiveValue: { [weak self ] _ in
+                //every 10 seconds, send the latest element as uniquely identified by bledeviceid
+                // we reset the newvalues so that we wont resend the same identifical element after 10 additional seconds
+                self?.newValues.forEach { el in
+                    self?.throttledSubject.send(el.value)
+                }
+                self?.newValues = [:]
+            }
+        )
+        .store(in: &timerBag)
+    }
+
+    public func stopTimer() {
+        if !timerBag.isEmpty {
+            timerBag.forEach { cancel in
+                cancel.cancel()
+            }
+        }
+    }
+
+    private func setupDebugListener() {
+        throttledSubject
+        .sink { el in
+            let now = Date().description
+            print("\(now) \t throttledPublisher got new value: \(el) ")
+        }
+        .store(in: &bag)
+    }
+
+    private func setupIncoming() {
+        incoming
+        .sink { [weak self] el in
+            guard let self = self else {
+                return
+            }
+
+            let id = el.self[keyPath: self.identificator]
+
+
+            let neverPublished = !self.initiallyPublished.contains(id)
+            if neverPublished {
+                self.initiallyPublished.insert(id)
+                //every element should be published initially
+                self.throttledSubject.send(el)
+                return
+            }
+
+            self.newValues[id] = el
+
+        }
+        .store(in: &bag)
+    }
+
+    init(identificator : KeyPath<T, U>, interval: TimeInterval) {
+        self.identificator = identificator
+        self.interval = interval
+
+        startTimer()
+        setupIncoming()
+
+
+        //this cancellable would normally not be used directly, as you would consume the publisher from swiftui
+        //setupDebugListener()
+
+    }
+
+    deinit {
+        print("deiniting GenericThrottler")
+    }
+
+
+}

+ 117 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/LibreTransmitterMetadata.swift

@@ -0,0 +1,117 @@
+//
+//  MiaoMiao.swift
+//  LibreMonitor
+//
+//  Created by Uwe Petersen on 02.11.18.
+//  Copyright © 2018 Uwe Petersen. All rights reserved.
+//
+
+import Foundation
+
+public struct LibreTransmitterMetadata: CustomStringConvertible {
+    // hardware number
+    public let hardware: String
+    // software number
+    public let firmware: String
+    // battery level, percentage between 0 % and 100 %
+    public let battery: Int?
+    // battery level String
+    public let batteryString: String
+
+    public let macAddress: String?
+
+    public let name: String
+
+    public let patchInfo: String?
+    public let uid: [UInt8]?
+
+    init(hardware: String, firmware: String, battery: Int?, name: String, macAddress: String?, patchInfo: String?, uid: [UInt8]?) {
+        self.hardware = hardware
+        self.firmware = firmware
+        self.battery = battery
+        let batteryString = battery == nil ? "-" : "\(battery!)"
+        self.batteryString = batteryString
+        self.macAddress = macAddress
+        self.name = name
+        self.patchInfo = patchInfo
+        self.uid = uid
+    }
+
+    public var description: String {
+        "Transmitter: \(name), Hardware: \(hardware), firmware: \(firmware)" +
+        "battery: \(batteryString), macAddress: \(String(describing: macAddress)), patchInfo: \(String(describing: patchInfo)), uid: \(String(describing: uid))"
+    }
+
+    public func sensorType() -> SensorType? {
+        guard let patchInfo = patchInfo else { return nil }
+        return SensorType(patchInfo: patchInfo)
+    }
+}
+
+extension String {
+    //https://stackoverflow.com/questions/39677330/how-does-string-substring-work-in-swift
+    //usage
+    //let s = "hello"
+    //s[0..<3] // "hel"
+    //s[3..<s.count] // "lo"
+    subscript(_ range: CountableRange<Int>) -> String {
+        let idx1 = index(startIndex, offsetBy: max(0, range.lowerBound))
+        let idx2 = index(startIndex, offsetBy: min(self.count, range.upperBound))
+        return String(self[idx1..<idx2])
+    }
+
+    func hexadecimal() -> Data? {
+        var data = Data(capacity: count / 2)
+
+        let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
+        regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in
+            let byteString = (self as NSString).substring(with: match!.range)
+            let num = UInt8(byteString, radix: 16)!
+            data.append(num)
+        }
+
+        guard data.count > 0 else { return nil }
+
+        return data
+    }
+}
+
+public enum SensorType: String, CustomStringConvertible {
+    case libre1    = "DF"
+    case libre1A2 =  "A2"
+    case libre2    = "9D"
+    case libreUS14day   = "E5"
+    case libreProH = "70"
+
+    public var description: String {
+        switch self {
+        case .libre1:
+            return "Libre 1"
+        case .libre1A2:
+            return "Libre 1 A2"
+        case .libre2:
+            return "Libre 2"
+        case .libreUS14day:
+            return "Libre US"
+        case .libreProH:
+            return "Libre PRO H"
+        }
+    }
+}
+
+public extension SensorType {
+    init?(patchInfo: String) {
+        guard patchInfo.count > 1 else { return nil }
+
+        let start = patchInfo[0..<2].uppercased()
+
+        let choices: [String: SensorType] = ["DF": .libre1, "A2": .libre1A2, "9D": .libre2, "E5": .libreUS14day, "70": .libreProH]
+
+        if let res = choices[start] {
+            self = res
+            return
+        }
+
+        return nil
+    }
+}

+ 207 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/BubbleTransmitter.swift

@@ -0,0 +1,207 @@
+//
+//  BubbleTransmitter.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 08/01/2020.
+//  Copyright © 2020 Bjørn Inge Berg. All rights reserved.
+//
+
+import CoreBluetooth
+import Foundation
+import UIKit
+import os.log
+
+public enum BubbleResponseType: UInt8 {
+    case dataPacket = 130
+    case bubbleInfo = 128 // = wakeUp + device info
+    case noSensor = 191
+    case serialNumber = 192
+    case patchInfo = 193 //0xC1
+     /// bubble firmware 2.6 support decrypt libre2 344 to libre1 344
+     /// if firmware >= 2.6, write [0x08, 0x01, 0x00, 0x00, 0x00, 0x2B]
+     /// bubble will decrypt the libre2 data and return it
+    //we don't really support decrypteddatapacket as we like to decrypt our selves!
+    //case decryptedDataPacket = 136 // 0x88
+}
+
+extension BubbleResponseType {
+    var description: String {
+        switch self {
+        case .bubbleInfo:
+            return "bubbleinfo"
+        case .dataPacket:
+            return "datapacket"
+        case .noSensor:
+            return "nosensor"
+        case .serialNumber:
+            return "serialnumber"
+        case .patchInfo:
+            return "patchInfo"
+        }
+    }
+}
+
+// The Bubble uses the same serviceUUID,
+// writeCharachteristic and notifyCharachteristic
+// as the MiaoMiao, but different byte sequences
+class BubbleTransmitter: MiaoMiaoTransmitter {
+    override class var shortTransmitterName: String {
+        "bubble"
+    }
+    override class var manufacturerer: String {
+        "bubbledevteam"
+    }
+
+    override class var smallImage: UIImage? {
+         UIImage(named: "bubble", in: Bundle.module, compatibleWith: nil)
+    }
+
+    override static func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool {
+        peripheral.name?.lowercased().starts(with: "bubble") ?? false
+    }
+
+    override func reset() {
+        rxBuffer.resetAllBytes()
+    }
+
+    override class var requiresDelayedReconnect : Bool {
+        true
+    }
+
+    private var hardware: String? = ""
+    private var firmware: String? = ""
+    private var mac: String? = ""
+
+    private var patchInfo: String?
+    private var uid: [UInt8]?
+
+    private var battery: Int?
+
+    fileprivate lazy var bLogger = Logger(forType: Self.self)
+
+    override class func getDeviceDetailsFromAdvertisement(advertisementData: [String: Any]?) -> String? {
+        let (amac, afirmware, ahardware) = Self.getDeviceDetailsFromAdvertisementInternal(advertisementData: advertisementData)
+
+        if let amac = amac, let ahardware = ahardware, let afirmware = afirmware {
+            return "\(amac)\n HW:\(ahardware), FW: \(afirmware)"
+        }
+
+        return nil
+    }
+
+    private static func getDeviceDetailsFromAdvertisementInternal(advertisementData: [String: Any]?) -> (String?, String?, String?) {
+
+        
+
+        guard let data = advertisementData?["kCBAdvDataManufacturerData"] as? Data else {
+            return (nil, nil, nil)
+        }
+        var mac = ""
+        for i in 0 ..< 6 {
+            mac += data.subdata(in: (7 - i)..<(8 - i)).hexEncodedString().uppercased()
+            if i != 5 {
+                mac += ":"
+            }
+        }
+
+        guard  data.count >= 12 else {
+            return (nil, nil, nil)
+        }
+
+        let fSub1 = Data(repeating: data[8], count: 1)
+        let fSub2 = Data(repeating: data[9], count: 1)
+        let firmware = Float("\(fSub1.hexEncodedString()).\(fSub2.hexEncodedString())")?.description
+
+        let hSub1 = Data(repeating: data[10], count: 1)
+        let hSub2 = Data(repeating: data[11], count: 1)
+
+        let hardware = Float("\(hSub1.hexEncodedString()).\(hSub2.hexEncodedString())")?.description
+        return (mac, firmware, hardware)
+    }
+
+    required init(delegate: LibreTransmitterDelegate, advertisementData: [String: Any]?) {
+        //advertisementData is unknown for the miaomiao
+
+        super.init(delegate: delegate, advertisementData: advertisementData)
+        //self.delegate = delegate
+        //deviceFromAdvertisementData(advertisementData: advertisementData)
+        (self.mac, self.firmware, self.hardware) = Self.getDeviceDetailsFromAdvertisementInternal(advertisementData: advertisementData)
+    }
+
+    override func requestData(writeCharacteristics: CBCharacteristic, peripheral: CBPeripheral) {
+        bLogger.debug("dabear:: bubbleRequestData")
+        reset()
+
+        peripheral.writeValue(Data([0x00, 0x00, 0x05]), for: writeCharacteristics, type: .withResponse)
+    }
+    override func updateValueForNotifyCharacteristics(_ value: Data, peripheral: CBPeripheral, writeCharacteristic: CBCharacteristic?) {
+        bLogger.debug("dabear:: bubbleDidUpdateValueForNotifyCharacteristics, firstbyte is: \(value.first.debugDescription)")
+        guard let firstByte = value.first, let bubbleResponseState = BubbleResponseType(rawValue: firstByte) else {
+           return
+        }
+        bLogger.debug("dabear:: bubble responsestate is of type \(bubbleResponseState.description)")
+        bLogger.debug("dabear:: bubble value is: \(value.toDebugString())")
+        switch bubbleResponseState {
+        case .bubbleInfo:
+            hardware = value[value.count-2].description + "." + value[value.count-1].description
+            firmware = value[2].description + "." + value[3].description
+           //let patchInfo = Data(Double(firmware)! < 1.35 ? value[3...8] : value[5...10])
+            battery = Int(value[4])
+
+            bLogger.debug("dabear:: Got bubbledevice: \(self.metadata.debugDescription)")
+           if let writeCharacteristic = writeCharacteristic {
+               
+               peripheral.writeValue(Data([0x02, 0x00, 0x00, 0x00, 0x00, 0x2B]), for: writeCharacteristic, type: .withResponse)
+           }
+        case .dataPacket://, .decryptedDataPacket:
+           rxBuffer.append(value.suffix(from: 4))
+            bLogger.debug("dabear:: aggregated datapacket is now of length: \(self.rxBuffer.count)")
+           if rxBuffer.count >= 352 {
+               handleCompleteMessage()
+               reset()
+           }
+        case .noSensor:
+            delegate?.libreTransmitterReceivedMessage(0x0000, txFlags: 0x34, payloadData: rxBuffer)
+
+            reset()
+        case .serialNumber:
+            guard value.count >= 10 else { return }
+            reset()
+            self.uid = [UInt8](value.subdata(in: 2..<10))
+
+            //for historical reasons
+            rxBuffer.append(value.subdata(in: 2..<10))
+
+        case .patchInfo:
+            guard value.count >= 10 else {
+                bLogger.debug("not able to extract patchinfo")
+                return
+            }
+            patchInfo = value.subdata(in: 5 ..< 11).hexEncodedString().uppercased()
+        }
+    }
+
+    private var rxBuffer = Data()
+    private var sensorData: SensorData?
+    private var metadata: LibreTransmitterMetadata?
+
+    override func handleCompleteMessage() {
+        bLogger.debug("dabear:: bubbleHandleCompleteMessage")
+
+        guard rxBuffer.count >= 352 else {
+            return
+        }
+
+        metadata = .init(hardware: hardware ?? "unknown", firmware: firmware ?? "unknown", battery: battery ?? 100, name: Self.shortTransmitterName, macAddress: self.mac, patchInfo: patchInfo, uid: self.uid)
+
+        let data = rxBuffer.subdata(in: 8..<352)
+        bLogger.debug("dabear:: bubbleHandleCompleteMessage raw data: \([UInt8](self.rxBuffer))")
+        sensorData = SensorData(uuid: rxBuffer.subdata(in: 0..<8), bytes: [UInt8](data), date: Date())
+
+        bLogger.debug("dabear:: bubble got sensordata \(self.sensorData.debugDescription) and metadata \(self.metadata.debugDescription), delegate is \(self.delegate.debugDescription)")
+
+        if let sensorData = sensorData, let metadata = metadata {
+            delegate?.libreTransmitterDidUpdate(with: sensorData, and: metadata)
+        }
+    }
+}

+ 158 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/Libre2DirectTransmitter.swift

@@ -0,0 +1,158 @@
+//
+//  Libre2DirectTransmitter.swift
+
+
+import CoreBluetooth
+import Foundation
+import os.log
+import UIKit
+
+
+
+
+class Libre2DirectTransmitter: LibreTransmitterProxyProtocol {
+    
+
+    fileprivate lazy var logger = Logger(forType: Self.self)
+
+    func reset() {
+        rxBuffer.resetAllBytes()
+    }
+
+    class var manufacturerer: String {
+        "Abbott"
+    }
+
+    class var smallImage: UIImage? {
+        UIImage(named: "libresensor", in: Bundle.module, compatibleWith: nil)
+    }
+
+    class var shortTransmitterName: String {
+        "libre2"
+    }
+
+    class var requiresDelayedReconnect : Bool {
+        false
+    }
+
+    private let expectedBufferSize = 46
+    static var requiresSetup = true
+    static var requiresPhoneNFC: Bool = true
+
+    static var writeCharacteristic: UUIDContainer? = "F001"//0000f001-0000-1000-8000-00805f9b34fb"
+    static var notifyCharacteristic: UUIDContainer? = "F002"//"0000f002-0000-1000-8000-00805f9b34fb"
+    //static var serviceUUID: [UUIDContainer] = ["0000fde3-0000-1000-8000-00805f9b34fb"]
+    static var serviceUUID: [UUIDContainer] = ["FDE3"]
+
+    weak var delegate: LibreTransmitterDelegate?
+
+    private var rxBuffer = Data()
+    private var sensorData: SensorData?
+    private var metadata: LibreTransmitterMetadata?
+
+    class func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool {
+        peripheral.name?.lowercased().starts(with: "abbott") ?? false
+    }
+
+    class func getDeviceDetailsFromAdvertisement(advertisementData: [String: Any]?) -> String? {
+        nil
+    }
+
+    required init(delegate: LibreTransmitterDelegate, advertisementData: [String: Any]?) {
+        //advertisementData is unknown for the miaomiao
+        self.delegate = delegate
+    }
+
+    func requestData(writeCharacteristics: CBCharacteristic, peripheral: CBPeripheral) {
+        //because of timing issues, we cannot use this method on libre2 eu sensors
+    }
+
+    func updateValueForNotifyCharacteristics(_ value: Data, peripheral: CBPeripheral, writeCharacteristic: CBCharacteristic?) {
+        rxBuffer.append(value)
+
+        logger.debug("libre2 direct Appended value with length  \(String(describing: value.count)), buffer length is: \(String(describing: self.rxBuffer.count))")
+
+
+        if rxBuffer.count == expectedBufferSize {
+            handleCompleteMessage()
+        }
+
+
+    }
+
+
+
+    func didDiscoverWriteCharacteristics(_ peripheral: CBPeripheral, writeCharacteristics: CBCharacteristic) {
+
+        guard let unlock = unlock() else{
+            logger.debug("Cannot unlock sensor, aborting")
+            return
+        }
+
+        logger.debug("Writing streaming unlock code to peripheral: \(unlock.hexEncodedString())")
+        peripheral.writeValue(unlock, for: writeCharacteristics, type: .withResponse)
+
+
+    }
+
+
+
+    func didDiscoverNotificationCharacteristic(_ peripheral: CBPeripheral, notifyCharacteristic: CBCharacteristic) {
+
+        logger.debug("libre2: saving notifyCharacteristic")
+        //peripheral.setNotifyValue(true, for: notifyCharacteristic)
+        logger.debug("libre2 setting notify while discovering : \(String(describing: notifyCharacteristic.debugDescription))")
+        peripheral.setNotifyValue(true, for: notifyCharacteristic)
+    }
+
+
+    private func unlock() -> Data? {
+
+        guard var sensor = UserDefaults.standard.preSelectedSensor else {
+            logger.debug("impossible to unlock sensor")
+            return nil
+        }
+
+        sensor.unlockCount = sensor.unlockCount + 1
+
+        UserDefaults.standard.preSelectedSensor = sensor
+
+        let unlockPayload = Libre2.streamingUnlockPayload(sensorUID: sensor.uuid, info: sensor.patchInfo, enableTime: 42, unlockCount: UInt16(sensor.unlockCount))
+        return Data(unlockPayload)
+
+    }
+
+    func handleCompleteMessage() {
+        guard rxBuffer.count >= expectedBufferSize else {
+            logger.debug("libre2 handle complete message with incorrect buffersize")
+            reset()
+            return
+        }
+
+        guard let sensor = UserDefaults.standard.preSelectedSensor else {
+            logger.debug("libre2 handle complete message without sensorinfo present")
+            reset()
+            return
+        }
+
+
+        do {
+            let decryptedBLE = Data(try Libre2.decryptBLE(id: [UInt8](sensor.uuid), data: [UInt8](rxBuffer)))
+            let sensorUpdate = Libre2.parseBLEData(decryptedBLE)
+
+            metadata = LibreTransmitterMetadata(hardware: "-", firmware: "-", battery: 100, name:  Self.shortTransmitterName, macAddress: nil, patchInfo: sensor.patchInfo.hexEncodedString().uppercased(), uid: [UInt8](sensor.uuid))
+
+            delegate?.libreSensorDidUpdate(with: sensorUpdate, and: metadata!)
+
+            print("libre2 got sensorupdate: \(String(describing: sensorUpdate))")
+
+        } catch {
+
+        }
+
+        reset()
+
+    }
+
+
+}

+ 729 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/LibreTransmitterProxyManager.swift

@@ -0,0 +1,729 @@
+//
+//  MiaoMiaoManager.swift
+//  LibreMonitor
+//
+//  Created by Uwe Petersen on 10.03.18, heravily modified by Bjørn Berg.
+//  Copyright © 2018 Uwe Petersen. All rights reserved.
+//
+
+import CoreBluetooth
+import Foundation
+import HealthKit
+import os.log
+import UIKit
+
+public enum BluetoothmanagerState: String {
+    case Unassigned = "Unassigned"
+    case Scanning = "Scanning"
+    case Disconnected = "Disconnected"
+    case DelayedReconnect = "Will soon reconnect"
+    case DisconnectingDueToButtonPress = "Disconnecting due to button press"
+    case Connecting = "Connecting"
+    case Connected = "Connected"
+    case Notifying = "Notifying"
+    case powerOff = "powerOff"
+    case UnknownDevice = "UnknownDevice"
+}
+
+public protocol LibreTransmitterDelegate: AnyObject {
+    // Can happen on any queue
+    func libreTransmitterStateChanged(_ state: BluetoothmanagerState)
+    func libreTransmitterReceivedMessage(_ messageIdentifier: UInt16, txFlags: UInt8, payloadData: Data)
+    // Will always happen on managerQueue
+    func libreTransmitterDidUpdate(with sensorData: SensorData, and Device: LibreTransmitterMetadata)
+    func libreSensorDidUpdate(with bleData: Libre2.LibreBLEResponse, and Device: LibreTransmitterMetadata)
+
+    func noLibreTransmitterSelected()
+    func libreManagerDidRestoreState(found peripherals: [CBPeripheral], connected to: CBPeripheral?)
+}
+
+extension LibreTransmitterDelegate {
+    func noLibreTransmitterSelected() {}
+    public func libreManagerDidRestoreState(found peripherals: [CBPeripheral], connected to: CBPeripheral?) {}
+}
+
+final class LibreTransmitterProxyManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, LibreTransmitterDelegate {
+    func libreSensorDidUpdate(with bleData: Libre2.LibreBLEResponse, and Device: LibreTransmitterMetadata) {
+        dispatchToDelegate { manager in
+            manager.delegate?.libreSensorDidUpdate(with: bleData, and: Device)
+        }
+    }
+
+
+
+    func libreManagerDidRestoreState(found peripherals: [CBPeripheral], connected to: CBPeripheral?) {
+        dispatchToDelegate { manager in
+            manager.delegate?.libreManagerDidRestoreState(found: peripherals, connected: to)
+        }
+    }
+
+    func noLibreTransmitterSelected() {
+        dispatchToDelegate { manager in
+            manager.delegate?.noLibreTransmitterSelected()
+        }
+    }
+
+    func libreTransmitterStateChanged(_ state: BluetoothmanagerState) {
+
+        logger.debug("libreTransmitterStateChanged delegating")
+        dispatchToDelegate { manager in
+           manager.delegate?.libreTransmitterStateChanged(state)
+        }
+    }
+
+    func libreTransmitterReceivedMessage(_ messageIdentifier: UInt16, txFlags: UInt8, payloadData: Data) {
+
+        logger.debug("libreTransmitterReceivedMessage delegating")
+        dispatchToDelegate { manager in
+            manager.delegate?.libreTransmitterReceivedMessage(messageIdentifier, txFlags: txFlags, payloadData: payloadData)
+        }
+    }
+
+    func libreTransmitterDidUpdate(with sensorData: SensorData, and Device: LibreTransmitterMetadata) {
+        self.metadata = Device
+        self.sensorData = sensorData
+
+        logger.debug("libreTransmitterDidUpdate delegating")
+        dispatchToDelegate { manager in
+            manager.delegate?.libreTransmitterDidUpdate(with: sensorData, and: Device)
+        }
+    }
+
+    // MARK: - Properties
+    private var wantsToTerminate = false
+    //private var lastConnectedIdentifier : String?
+
+    var activePlugin: LibreTransmitterProxyProtocol? = nil {
+        didSet {
+
+            logger.debug("dabear:: activePlugin changed from \(oldValue.debugDescription) to \(self.activePlugin.debugDescription)")
+            
+        }
+    }
+
+    var activePluginType: LibreTransmitterProxyProtocol.Type? {
+        activePlugin?.staticType
+    }
+
+    var shortTransmitterName: String? {
+        activePluginType?.shortTransmitterName
+    }
+
+
+    fileprivate lazy var logger = Logger(forType: Self.self)
+    var metadata: LibreTransmitterMetadata?
+
+    var centralManager: CBCentralManager!
+    var peripheral: CBPeripheral?
+    //    var slipBuffer = SLIPBuffer()
+    var writeCharacteristic: CBCharacteristic?
+
+    var sensorData: SensorData?
+
+    public var identifier: UUID? {
+        peripheral?.identifier
+    }
+
+    private let managerQueue = DispatchQueue(label: "no.bjorninge.bluetoothManagerQueue", qos: .utility)
+    private let delegateQueue = DispatchQueue(label: "no.bjorninge.delegateQueue", qos: .utility)
+
+    fileprivate var serviceUUIDs: [CBUUID]? {
+        activePluginType?.serviceUUID.map { $0.value }
+    }
+    fileprivate var writeCharachteristicUUID: CBUUID? {
+        activePluginType?.writeCharacteristic?.value
+    }
+    fileprivate var notifyCharacteristicUUID: CBUUID? {
+        activePluginType?.notifyCharacteristic?.value
+    }
+
+    weak var delegate: LibreTransmitterDelegate? {
+        didSet {
+           dispatchToDelegate { manager in
+                // Help delegate initialize by sending current state directly after delegate assignment
+                manager.delegate?.libreTransmitterStateChanged(self.state)
+           }
+        }
+    }
+
+    private var state: BluetoothmanagerState = .Unassigned {
+        didSet {
+            dispatchToDelegate { manager in
+                // Help delegate initialize by sending current state directly after delegate assignment
+                manager.delegate?.libreTransmitterStateChanged(self.state)
+            }
+        }
+    }
+    public var connectionStateString: String {
+        self.state.rawValue
+    }
+
+    public func dispatchToDelegate( _ closure :@escaping  (_ aself: LibreTransmitterProxyManager) -> Void ) {
+        delegateQueue.async { [weak self] in
+            if let self = self {
+                closure(self)
+            }
+        }
+    }
+
+    // MARK: - Methods
+
+    override init() {
+        super.init()
+        logger.debug("LibreTransmitterProxyManager called")
+        managerQueue.sync {
+            let restoreID = (bundleSeedID() ?? "Unknown") + "BluetoothRestoreIdentifierKey"
+            centralManager = CBCentralManager(delegate: self, queue: managerQueue, options: [CBCentralManagerOptionShowPowerAlertKey: true, CBCentralManagerOptionRestoreIdentifierKey: restoreID])
+        }
+    }
+
+    func scanForDevices() {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+
+        logger.debug("Scan for bluetoothdevice while internal state= \(String(describing: self.state)), bluetoothstate=\(String(describing: self.centralManager.state))")
+
+        guard centralManager.state == .poweredOn else {
+            return
+        }
+
+        logger.debug("Before scan for libre bluetooth device while central manager state was  \(String(describing: self.centralManager.state.rawValue)))")
+
+        let scanForAllServices = false
+
+        //this will search for all peripherals. Guaranteed to work
+        if scanForAllServices {
+
+            logger.debug("Scanning for all services:")
+            centralManager.scanForPeripherals(withServices: nil, options: nil)
+        } else {
+            // This is what we should have done
+            // Here we optimize by scanning only for relevant services
+            // However, this doesn't work correctly with both miaomiao and bubble
+            let services = LibreTransmitters.all.getServicesForDiscovery()
+            logger.debug("Scanning for specific services: \(String(describing: services.map { $0.uuidString }))")
+            centralManager.scanForPeripherals(withServices: services, options: nil)
+
+        }
+
+        state = .Scanning
+    }
+
+    private func reset() {
+        logger.debug("manager is resetting the activeplugin")
+
+        self.activePlugin?.reset()
+    }
+
+    private func connect(force forceConnect: Bool = false, advertisementData: [String: Any]?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+        logger.debug("connect while state: \(String(describing: self.state.rawValue))")
+        if centralManager.isScanning {
+            centralManager.stopScan()
+        }
+        if state == .DisconnectingDueToButtonPress && !forceConnect {
+
+            logger.debug("Connect aborted, user has actively disconnected and a reconnect was not forced")
+            return
+        }
+
+        if let peripheral = self.peripheral {
+            peripheral.delegate = self
+
+            if activePlugin?.canSupportPeripheral(peripheral) == true {
+                //when reaching this part,
+                //we are sure the peripheral is reconnecting and therefore needs reset
+
+                logger.debug("Connecting to known device with known plugin")
+
+                self.reset()
+
+                centralManager.connect(peripheral, options: nil)
+                state = .Connecting
+            } else if let plugin = LibreTransmitters.getSupportedPlugins(peripheral)?.first {
+                self.activePlugin = plugin.init(delegate: self, advertisementData: advertisementData)
+
+                logger.debug("Connecting to new device with known plugin")
+
+                //only connect to devices we can support (i.e. devices that has a suitable plugin)
+                centralManager.connect(peripheral, options: nil)
+                state = .Connecting
+            } else {
+                state = .UnknownDevice
+            }
+        }
+    }
+
+    func disconnectManually() {
+        dispatchPrecondition(condition: .notOnQueue(managerQueue))
+        logger.debug("Disconnect manually while state \(String(describing: self.state.rawValue))" )
+
+        managerQueue.sync {
+            switch self.state {
+            case .Connected, .Connecting, .Notifying, .Scanning:
+                self.state = .DisconnectingDueToButtonPress  // to avoid reconnect in didDisconnetPeripheral
+
+                self.wantsToTerminate = true
+            default:
+                break
+            }
+
+            if centralManager.isScanning {
+                logger.debug("Stopping scan")
+                centralManager.stopScan()
+            }
+            if let peripheral = peripheral {
+                centralManager.cancelPeripheralConnection(peripheral)
+            }
+        }
+    }
+
+    // MARK: - CBCentralManagerDelegate
+
+    func centralManagerDidUpdateState(_ central: CBCentralManager) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+        logger.debug("Central Manager did update state to \(String(describing: central.state.rawValue))")
+
+        switch central.state {
+        case .poweredOff:
+            state = .powerOff
+        case .resetting, .unauthorized, .unknown, .unsupported:
+            logger.debug("Central Manager was either .poweredOff, .resetting, .unauthorized, .unknown, .unsupported:  \(String(describing: central.state))")
+            state = .Unassigned
+
+            if central.state == .resetting, let peripheral = self.peripheral {
+                logger.debug("Central Manager resetting, will cancel peripheral connection")
+                central.cancelPeripheralConnection(peripheral)
+                self.peripheral = nil
+            }
+
+            if central.isScanning {
+                central.stopScan()
+            }
+        case .poweredOn:
+
+            if state == .DisconnectingDueToButtonPress {
+                logger.debug("Central Manager was powered on but sensorstate was DisconnectingDueToButtonPress: \(String(describing: central.state))")
+
+                return
+            }
+
+            logger.debug("Central Manager was powered on")
+
+
+            //not sure if needed, but can be helpful when state is restored
+            if let peripheral = peripheral, delegate != nil {
+                // do not scan if already connected
+                switch peripheral.state {
+                case .disconnected, .disconnecting:
+                    logger.debug("Central Manager was powered on, peripheral state is disconnecting")
+                    self.connect(advertisementData: nil)
+                case .connected, .connecting:
+                    logger.debug("Central Manager was powered on, peripheral state is connected/connecting, renewing plugin")
+
+                    // This is necessary
+                    // Normally the connect() method would have set the correct plugin,
+                    // however when we hit this path, it is likely a state restoration
+                    if self.activePlugin == nil || self.activePlugin?.canSupportPeripheral(peripheral) == false {
+                        let plugin = LibreTransmitters.getSupportedPlugins(peripheral)?.first
+                        self.activePlugin = plugin?.init(delegate: self, advertisementData: nil)
+
+
+                        logger.debug("Central Manager was powered on, peripheral state is connected/connecting, stopping scan")
+                        if central.isScanning && peripheral.state == .connected {
+                            central.stopScan()
+                        }
+                        if peripheral.delegate == nil {
+                            logger.debug("Central Manager was powered on, peripheral delegate was nil")
+
+                        }
+                    }
+
+                    if let serviceUUIDs = serviceUUIDs, !serviceUUIDs.isEmpty {
+                        peripheral.discoverServices(serviceUUIDs) // good practice to just discover the services, needed
+                    } else {
+                        logger.debug("Central Manager was powered on, could not discover services")
+
+                    }
+
+                default:
+                    logger.debug("Central Manager already connected")
+                }
+            } else {
+
+
+                if let preselected = UserDefaults.standard.preSelectedDevice,
+                   let uuid = UUID(uuidString: preselected),
+                   let newPeripheral = centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
+                   let plugin = LibreTransmitters.getSupportedPlugins(newPeripheral)?.first {
+                    logger.debug("Central Manager was powered on, directly connecting to already known peripheral \(newPeripheral): \(String(describing: self.state))")
+                    self.peripheral = newPeripheral
+                    self.peripheral?.delegate = self
+
+                    self.activePlugin = plugin.init(delegate: self, advertisementData: nil)
+
+                    managerQueue.async {
+                        self.state = .Connecting
+                        self.centralManager.connect(newPeripheral, options: nil)
+
+                    }
+
+                } else {
+                    //state should be nassigned here
+                    logger.debug("Central Manager was powered on, scanningfordevice: \(String(describing: self.state))")
+                    scanForDevices() // power was switched on, while app is running -> reconnect.
+
+                }
+
+
+            }
+        @unknown default:
+            fatalError("libre bluetooth state unhandled")
+        }
+    }
+
+    func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+        logger.debug("Central Manager will restore state to \(String(describing: dict.debugDescription))")
+
+
+        guard self.peripheral == nil else {
+            logger.debug("Central Manager tried to restore state while already connected")
+            return
+        }
+
+        guard let preselected = UserDefaults.standard.preSelectedDevice else {
+            logger.debug("Central Manager tried to restore state but no device was preselected")
+            return
+        }
+
+        guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
+            logger.debug("Central Manager tried to restore state but no peripheral found")
+            self.scanForDevices()
+            return
+        }
+
+        defer {
+            self.libreManagerDidRestoreState(found: peripherals, connected: self.peripheral)
+        }
+
+        let restorablePeripheral = peripherals.first(where: { $0.identifier.uuidString == preselected })
+
+        guard let peripheral = restorablePeripheral else {
+            return
+        }
+
+        self.peripheral = peripheral
+        peripheral.delegate = self
+
+        switch peripheral.state {
+        case .disconnected, .disconnecting:
+            logger.debug("Central Manager tried to restore state from disconnected peripheral")
+            state = .Disconnected
+            self.connect(advertisementData: nil)
+        case .connecting:
+            logger.debug("Central Manager tried to restore state from connecting peripheral")
+            state = .Connecting
+        case .connected:
+            logger.debug("Central Manager tried to restore state from connected peripheral, letting centralManagerDidUpdateState() do the rest of the job")
+            //the idea here is to let centralManagerDidUpdateState() do the heavy lifting
+            // after all, we did assign the periheral.delegate to self earlier
+
+            //that means the following is not necessary:
+            //state = .Connected
+            //peripheral.discoverServices(serviceUUIDs) // good practice to just discover the services, needed
+        @unknown default:
+            fatalError("Failed due to unkown default, Uwe!")
+        }
+    }
+
+    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+
+        logger.debug("Did discover peripheral while state \(String(describing: self.state.rawValue)) with name: \(String(describing: peripheral.name)), wantstoterminate?: \(self.wantsToTerminate)")
+
+
+        // Libre2:
+        // during setup, we find the uid by scanning via nfc
+        // first time connecting to a libre2 sensor via bluetooth we don't know its peripheral identifier
+        // but since the uid is also part of the libre 2bluetooth advertismentdata we trade uid for
+        
+         if let selectedUid = UserDefaults.standard.preSelectedUid {
+            logger.debug("Was asked to connect preselected libre2 by uid: \(selectedUid.hex), discovered devicename is: \(String(describing: peripheral.name))")
+
+            guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data else {
+                return
+            }
+
+            guard manufacturerData.count == 8 else {
+                return
+            }
+
+            var foundUUID = manufacturerData.subdata(in: 2..<8)
+            foundUUID.append(contentsOf: [0x07, 0xe0])
+
+            guard foundUUID == selectedUid && Libre2DirectTransmitter.canSupportPeripheral(peripheral) else {
+                return
+            }
+
+            //next time we search via bluetooth, let's identify the sensor with its bluetooth identifier
+            UserDefaults.standard.preSelectedUid = nil
+            UserDefaults.standard.preSelectedDevice = peripheral.identifier.uuidString
+
+            logger.debug("ManufacturerData: \(manufacturerData), found uid: \(foundUUID)")
+
+            logger.debug("Did connect to preselected \(String(describing: peripheral.name)) with identifier \(String(describing: peripheral.identifier.uuidString)) and uid \(selectedUid.hex)")
+            self.peripheral = peripheral
+
+            self.connect(force: true, advertisementData: advertisementData)
+
+
+            return
+
+        }
+
+        if let preselected = UserDefaults.standard.preSelectedDevice {
+            if peripheral.identifier.uuidString == preselected {
+                logger.debug("Did connect to preselected \(String(describing: peripheral.name)) with identifier \(String(describing: peripheral.identifier.uuidString))")
+                self.peripheral = peripheral
+
+                self.connect(force: true, advertisementData: advertisementData)
+            } else {
+                logger.info("Did not connect to \(String(describing: peripheral.name)) with identifier \(String(describing: peripheral.identifier.uuidString)), because another device with identifier \(preselected) was selected")
+            }
+
+            return
+        } else {
+            self.noLibreTransmitterSelected()
+        }
+    }
+
+    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+
+        logger.debug("Did connect peripheral while state \(String(describing: self.state.rawValue)) with name: \(String(describing: peripheral.name))")
+        if central.isScanning {
+            central.stopScan()
+        }
+        state = .Connected
+        //self.lastConnectedIdentifier = peripheral.identifier.uuidString
+        // Discover all Services. This might be helpful if writing is needed some time
+        peripheral.discoverServices(serviceUUIDs)
+    }
+
+    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+
+        logger.debug("Did fail to connect peripheral while state: \(String(describing: self.state.rawValue))")
+        if let error = error {
+            logger.error("Did fail to connect peripheral error: \(error.localizedDescription)")
+        }
+        state = .Disconnected
+
+        self.reconnect()
+    }
+
+    private func reconnect() {
+        let withDelay = self.activePluginType?.requiresDelayedReconnect == true
+        if withDelay {
+            delayedReconnect()
+        } else {
+            reconnectImmediately()
+        }
+    }
+
+    private func reconnectImmediately() {
+        self.connect(advertisementData: nil)
+    }
+
+    private func delayedReconnect(_ seconds: Double = 7) {
+        state = .DelayedReconnect
+
+        logger.debug("Will reconnect peripheral in  \(String(describing: seconds)) seconds")
+        self.reset()
+        // attempt to avoid IOS killing app because of cpu usage.
+        // postpone connecting for x seconds
+        DispatchQueue.global(qos: .utility).async { [weak self] in
+            Thread.sleep(forTimeInterval: seconds)
+            self?.managerQueue.sync {
+                self?.connect(advertisementData: nil)
+            }
+        }
+    }
+
+    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+
+        logger.debug("Did disconnect peripheral while state: \(String(describing: self.state.rawValue)))")
+        if let error = error {
+            logger.error("Did disconnect peripheral error: \(error.localizedDescription)")
+        }
+
+        switch state {
+        case .DisconnectingDueToButtonPress:
+            state = .Disconnected
+            self.wantsToTerminate = true
+
+        default:
+            state = .Disconnected
+            self.reconnect()
+
+            //    scanForMiaoMiao()
+        }
+    }
+
+    // MARK: - CBPeripheralDelegate
+
+    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+        logger.debug("Did discover services. is plugin nil? \((self.activePlugin == nil ? "nil" : "not nil"))")
+        if let error = error {
+            logger.error("Did discover services error: \(error.localizedDescription)")
+        }
+
+        if let services = peripheral.services {
+            for service in services {
+                let toDiscover = [writeCharachteristicUUID, notifyCharacteristicUUID].compactMap { $0 }
+
+                logger.debug("Will discover : \(String(describing: toDiscover.count)) Characteristics for service \(String(describing: service.debugDescription))")
+
+                if !toDiscover.isEmpty {
+                    peripheral.discoverCharacteristics(toDiscover, for: service)
+
+                    logger.debug("Did discover service: \(String(describing: service.debugDescription))")
+                }
+            }
+        }
+    }
+
+    func didDiscoverNotificationCharacteristic(_ peripheral: CBPeripheral, notifyCharacteristic characteristic: CBCharacteristic){
+
+
+        logger.debug("Did discover characteristic: \(String(describing: characteristic.debugDescription)) and asking activeplugin to handle it as a notification Characteristic")
+
+        self.activePlugin?.didDiscoverNotificationCharacteristic(peripheral, notifyCharacteristic: characteristic)
+
+
+
+
+    }
+
+    func didDiscoverWriteCharacteristic(_ peripheral: CBPeripheral, writeCharacteristic characteristic: CBCharacteristic){
+        writeCharacteristic = characteristic
+        logger.debug("Did discover characteristic: \(String(describing: characteristic.debugDescription)) and asking activeplugin to handle it as a write Characteristic")
+        self.activePlugin?.didDiscoverWriteCharacteristics(peripheral, writeCharacteristics: characteristic)
+
+
+    }
+
+    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+
+        logger.debug("Did discover characteristics for service \(String(describing: peripheral.name))")
+
+        if let error = error {
+            logger.error("Did discover characteristics for service error: \(error.localizedDescription)")
+        }
+
+        if let characteristics = service.characteristics {
+            for characteristic in characteristics {
+
+                logger.debug("Did discover characteristic: \(String(describing: characteristic.debugDescription))")
+                if characteristic.properties.intersection(.notify) == .notify && characteristic.uuid == notifyCharacteristicUUID {
+                    didDiscoverNotificationCharacteristic(peripheral, notifyCharacteristic: characteristic)
+                }
+                if characteristic.uuid == writeCharachteristicUUID {
+                    didDiscoverWriteCharacteristic(peripheral, writeCharacteristic: characteristic)
+                }
+            }
+        } else {
+            logger.debug("Discovered characteristics, but no characteristics listed. There must be some error.")
+        }
+    }
+
+    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+        logger.debug("Did update notification state for characteristic: \(String(describing: characteristic.debugDescription))")
+
+        if let error = error {
+            logger.error("Peripheral did update notification state for characteristic: \(error.localizedDescription) with error")
+        } else {
+            self.reset()
+            requestData()
+        }
+        state = .Notifying
+    }
+
+    private var lastNotifyUpdate: Date?
+    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+
+        let now = Date()
+
+        // We can expect thedevices to complete well within 5 seconds for all the telegrams combined in a session
+        // it is therefore reasonable to expect the time between one telegram
+        // to the other in the same session to be well within 6 seconds
+        // this path will be hit when a telegram for some reason is dropped
+        // in a session. Or that the user disconnecting and reconnecting during a transmission
+        // By resetting here we ensure that the rxbuffer doesn't leak over into the next session
+        // Leaking over into the next session, is however not a problem for consitency as we always check the CRC's anyway
+        if let lastNotifyUpdate = self.lastNotifyUpdate, now > lastNotifyUpdate.addingTimeInterval(6) {
+            logger.debug("dabear:: there hasn't been any traffic to  the \((self.activePluginType?.shortTransmitterName).debugDescription) plugin for more than 10 seconds, so we reset now")
+            self.reset()
+        }
+
+        logger.debug("Did update value for characteristic: \(String(describing: characteristic.debugDescription))")
+
+        self.lastNotifyUpdate = now
+
+        if let error = error {
+            logger.error("Characteristic update error: \(error.localizedDescription)")
+        } else {
+            if characteristic.uuid == notifyCharacteristicUUID, let value = characteristic.value {
+                if self.activePlugin == nil {
+                    logger.error("Characteristic update error: activeplugin was nil")
+                }
+                self.activePlugin?.updateValueForNotifyCharacteristics(value, peripheral: peripheral, writeCharacteristic: writeCharacteristic)
+            }
+        }
+    }
+
+    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
+        dispatchPrecondition(condition: .onQueue(managerQueue))
+        logger.debug("Did Write value \(String(describing: characteristic.value?.hexEncodedString())) for characteristic \(String(characteristic.debugDescription))")
+        self.activePlugin?.didWrite(peripheral, characteristics: characteristic)
+
+    }
+
+    func requestData() {
+       guard let peripheral = peripheral,
+            let writeCharacteristic = writeCharacteristic else {
+                return
+        }
+        self.activePlugin?.requestData(writeCharacteristics: writeCharacteristic, peripheral: peripheral)
+    }
+
+    deinit {
+        self.activePlugin = nil
+        self.delegate = nil
+        logger.debug("dabear:: miaomiaomanager deinit called")
+    }
+}
+
+extension LibreTransmitterProxyManager {
+    public var manufacturer: String {
+        activePluginType?.manufacturerer ?? "n/a"
+    }
+
+    var device: HKDevice? {
+        HKDevice(
+            name: "MiaomiaoClient",
+            manufacturer: manufacturer,
+            model: nil, //latestSpikeCollector,
+            hardwareVersion: self.metadata?.hardware ,
+            firmwareVersion: self.metadata?.firmware,
+            softwareVersion: nil,
+            localIdentifier: identifier?.uuidString,
+            udiDeviceIdentifier: nil
+        )
+    }
+}

+ 89 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/LibreTransmitterProxyProtocol.swift

@@ -0,0 +1,89 @@
+//
+//  LibreTransmitter.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 08/01/2020.
+//  Copyright © 2020 Bjørn Inge Berg. All rights reserved.
+//
+
+import CoreBluetooth
+import Foundation
+import UIKit
+public protocol LibreTransmitterProxyProtocol: AnyObject {
+    static var shortTransmitterName: String { get }
+    static var smallImage: UIImage? { get }
+    static var manufacturerer: String { get }
+    static var requiresPhoneNFC: Bool { get }
+    static var requiresSetup : Bool { get }
+    static func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool
+
+    static var writeCharacteristic: UUIDContainer? { get set }
+    static var notifyCharacteristic: UUIDContainer? { get set }
+    static var serviceUUID: [UUIDContainer] { get set }
+
+    var delegate: LibreTransmitterDelegate? { get set }
+    init(delegate: LibreTransmitterDelegate, advertisementData: [String: Any]? )
+    func requestData(writeCharacteristics: CBCharacteristic, peripheral: CBPeripheral)
+    func updateValueForNotifyCharacteristics(_ value: Data, peripheral: CBPeripheral, writeCharacteristic: CBCharacteristic?)
+    func didDiscoverWriteCharacteristics(_ peripheral: CBPeripheral, writeCharacteristics: CBCharacteristic)
+    func didDiscoverNotificationCharacteristic(_ peripheral: CBPeripheral, notifyCharacteristic: CBCharacteristic)
+    func didWrite(_ peripheral: CBPeripheral, characteristics: CBCharacteristic) 
+
+    func reset()
+
+
+    
+
+    static func getDeviceDetailsFromAdvertisement(advertisementData: [String: Any]?) -> String?
+
+}
+
+extension LibreTransmitterProxyProtocol {
+    func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool {
+        Self.canSupportPeripheral(peripheral)
+    }
+    public var staticType: LibreTransmitterProxyProtocol.Type {
+        Self.self
+    }
+
+    func didDiscoverWriteCharacteristics(_ peripheral: CBPeripheral, writeCharacteristics: CBCharacteristic) {
+        
+    }
+
+    func didDiscoverNotificationCharacteristic(_ peripheral: CBPeripheral, notifyCharacteristic: CBCharacteristic) {
+        print("Setting setNotifyValue on notifyCharacteristic")
+        peripheral.setNotifyValue(true, for: notifyCharacteristic)
+    }
+
+    func didWrite(_ peripheral: CBPeripheral, characteristics: CBCharacteristic) {
+
+    }
+
+    static var requiresSetup : Bool { return false}
+    static var requiresPhoneNFC: Bool { return false }
+
+    static var requiresDelayedReconnect: Bool { return false}
+}
+
+extension Array where Array.Element == LibreTransmitterProxyProtocol.Type {
+    func getServicesForDiscovery() -> [CBUUID] {
+        self.flatMap {
+            return $0.serviceUUID.map { $0.value }
+        }.removingDuplicates()
+    }
+}
+
+public enum LibreTransmitters {
+    public static var all: [LibreTransmitterProxyProtocol.Type] {
+        [MiaoMiaoTransmitter.self, BubbleTransmitter.self, Libre2DirectTransmitter.self]
+    }
+    public static func isSupported(_ peripheral: CBPeripheral) -> Bool {
+        getSupportedPlugins(peripheral)?.isEmpty == false
+    }
+
+    public static func getSupportedPlugins(_ peripheral: CBPeripheral) -> [LibreTransmitterProxyProtocol.Type]? {
+        all.enumerated().compactMap {
+            $0.element.canSupportPeripheral(peripheral) ? $0.element : nil
+        }
+    }
+}

+ 337 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/MiaomiaoTransmitter.swift

@@ -0,0 +1,337 @@
+//
+//  MiaomiaoTransmitter.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 01/08/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+//  How does the MiaoMiao work?
+//
+//    0.) Advertising
+//        MiaoMiao advertises with the following data:
+//        - key : "kCBAdvDataIsConnectable"     - value : 1
+//        - key : "kCBAdvDataManufacturerData"  - value : <0034cb1c 53093fb4> -> This might be usable as a unique device id.
+//        - key : "kCBAdvDataLocalName"         - value : miaomiao
+//
+//    1.) Services
+///       The MiaoMiao has two bluetooth services, one provided for the open source community and one that is probably to be used by the Tomato app.
+//        a) UUID: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E -> Open Source Community
+//           Did discover service: <CBService: 0x1c4673a00, isPrimary = YES, UUID = 6E400001-B5A3-F393-E0A9-E50E24DCCA9E>
+//        b) UUID: 00001532-1212-EFDE-1523-785FEABCD123
+//           Did discover service: <CBService: 0x1c0a61880, isPrimary = YES, UUID = 00001530-1212-EFDE-1523-785FEABCD123>
+//
+//    2.) Characteristics for open source service with UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
+//
+//        The service contains two characheristics:
+//
+//          a) Notify_Characteristic
+//             UUID: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
+//             "<CBCharacteristic: 0x1c02ae7c0, UUID = 6E400003-B5A3-F393-E0A9-E50E24DCCA9E, properties = 0x10, value = (null), notifying = NO>"
+//                 ... with properties:
+//             __C.CBCharacteristicProperties(rawValue: 16)
+//             Broadcast:                            [false]
+//             Read:                                 [false]
+//             WriteWithoutResponse:                 [false]
+//             Write:                                [false]
+//             Notify:                               [true]
+//             Indicate:                             [false]
+//             AuthenticatedSignedWrites:            [false]
+//             ExtendedProperties:                   [false]
+//             NotifyEncryptionRequired:             [false]
+//             BroaIndicateEncryptionRequireddcast:  [false]
+//             Service for Characteristic:           ["<CBService: 0x1c087f940, isPrimary = YES, UUID = 6E400001-B5A3-F393-E0A9-E50E24DCCA9E>"]
+//
+//          b) Write_Characteristic
+//             UUID: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
+//             "<CBCharacteristic: 0x1c02a81c0, UUID = 6E400002-B5A3-F393-E0A9-E50E24DCCA9E, properties = 0xC, value = (null), notifying = NO>"
+//                 ... with properties:
+//             __C.CBCharacteristicProperties(rawValue: 12)
+//             Broadcast:                            [false]
+//             Read:                                 [false]
+//             WriteWithoutResponse:                 [true]
+//             Write:                                [true]
+//             Notify:                               [false]
+//             Indicate:                             [false]
+//             AuthenticatedSignedWrites:            [false]
+//             ExtendedProperties:                   [false]
+//             NotifyEncryptionRequired:             [false]
+//             BroaIndicateEncryptionRequireddcast:  [false]
+//             Service for Characteristic:           ["<CBService: 0x1c087f940, isPrimary = YES, UUID = 6E400001-B5A3-F393-E0A9-E50E24DCCA9E>"]
+//
+//      3.) Characteristics for (possibly) Tomato app services with UUID 00001532-1212-EFDE-1523-785FEABCD123
+//
+//          The service contains three characteristics
+//
+//          a) Read characteristic
+//             "<CBCharacteristic: 0x1c42a8c40, UUID = 00001534-1212-EFDE-1523-785FEABCD123, properties = 0x2, value = (null), notifying = NO>"
+//                 ... with properties:
+//             __C.CBCharacteristicProperties(rawValue: 2)
+//             Broadcast:                            [false]
+//             Read:                                 [true]
+//             WriteWithoutResponse:                 [false]
+//             Write:                                [false]
+//             Notify:                               [false]
+//             Indicate:                             [false]
+//             AuthenticatedSignedWrites:            [false]
+//             ExtendedProperties:                   [false]
+//             NotifyEncryptionRequired:             [false]
+//             BroaIndicateEncryptionRequireddcast:  [false]
+//             Service for Characteristic:           ["<CBService: 0x1c0a61880, isPrimary = YES, UUID = 00001530-1212-EFDE-1523-785FEABCD123>"]
+//
+//          b) Write without respons characteristic
+//             Characteristic:
+//             "<CBCharacteristic: 0x1c42a2220, UUID = 00001532-1212-EFDE-1523-785FEABCD123, properties = 0x4, value = (null), notifying = NO>"
+//             ... with properties:
+//             __C.CBCharacteristicProperties(rawValue: 4)
+//             Broadcast:                            [false]
+//             Read:                                 [false]
+//             WriteWithoutResponse:                 [true]
+//             Write:                                [false]
+//             Notify:                               [false]
+//             Indicate:                             [false]
+//             AuthenticatedSignedWrites:            [false]
+//             ExtendedProperties:                   [false]
+//             NotifyEncryptionRequired:             [false]
+//             BroaIndicateEncryptionRequireddcast:  [false]
+//             Service for Characteristic:           ["<CBService: 0x1c0a61880, isPrimary = YES, UUID = 00001530-1212-EFDE-1523-785FEABCD123>"]
+//
+//          c) Write and notify characteristic
+//             "<CBCharacteristic: 0x1c02a8220, UUID = 00001531-1212-EFDE-1523-785FEABCD123, properties = 0x18, value = (null), notifying = NO>"
+//                 ... with properties:
+//             __C.CBCharacteristicProperties(rawValue: 24)
+//             Broadcast:                            [false]
+//             Read:                                 [false]
+//             WriteWithoutResponse:                 [false]
+//             Write:                                [true]
+//             Notify:                               [true]
+//             Indicate:                             [false]
+//             AuthenticatedSignedWrites:            [false]
+//             ExtendedProperties:                   [false]
+//             NotifyEncryptionRequired:             [false]
+//             BroaIndicateEncryptionRequireddcast:  [false]
+//             Service for Characteristic:           ["<CBService: 0x1c0a61880, isPrimary = YES, UUID = 00001530-1212-EFDE-1523-785FEABCD123>"]
+//
+//  The MiaoMiao protocol
+//  1.) Data
+//      TX: 0xF0
+//          Request all the data or the sensor. The bluetooth will return the data at a certain frequency (default is every 5 minutes) after the request
+//      RX:
+//          a) Data (363 bytes):
+//             Pos.  0 (0x00): 0x28 +
+//             Pos.  1 (0x01): Len[2 bytes] +
+//             Pos.  3 (0x03): Index [2 bytes] (this is the minute counter of the Freestyle Libre sensor) +
+//             Pos.  5 (0x05): ID [8 bytes] +
+//             Pos. 13 (0x0D): xbattery level in percent [1 byte] (e.g. 0x64 which is 100 in decimal means 100%?)
+//             Pos. 14 (0x0E): firmware version [2 bytes] +
+//             Pos. 16 (0x10): hardware version [2 bytes] +
+//             Pos. 18 (0x12): FRAM data (43 x 8 bytes = 344 bytes) +
+//             Pos. end      : 0x29
+//             Example: 28  07b3  5457  db353e01 00a007e0  64  0034 0001  11b6e84f050003 875104 57540000 00 000000 00000000 0000b94b 060f1600 c0da6a80 1600c0d6 6a801600
+//                      0x28   -> marks begin of data response
+//                      0x07b3 -> len is 1971 bytes (= 1952 for FRAM and 19 bytes for all the rest from 0x28 to 0x29, both of which are included)
+//                                but as of 2018-03-12 only 1791 bytes are sent.
+//                      0x5457 -> index is 21591
+//                      0xdb353e0100a007e0 -> id, can be converted to serial number
+//                      0x64   -> battery level (= 100%)
+//                      0x0034 -> firmware version
+//                      0x0001 -> hardware version
+//                      0x11b6e84f05000387 FRAM block 0x00 (sensor is expired since byte 0x04 has value 0x05)
+//                      0x5104575400000000 FRAM block 0x01
+//                      0x0000000000000000 FRAM block 0x02
+//                      0xb94b060f1600c0da FRAM block 0x03
+//                       ...
+//            28 07b3 182b  9a8150 0100a007 e0640034 0001539d
+//          b) A new sensor has been detected
+//             0x32
+//          c) No sensor has been detected
+//             0x34
+//
+//  2.) Confirm to replace the sensor (if a new sensor is detected and shall be used, send this)
+//      TX: 0xD301
+//  3.) Confirm not to replace the sensor (if a new sensor is detected and shall not be used, send this)
+//      TX: 0xD300
+//  4.) Change the frequence of data transmission
+//      TX: 0xD1XX, where XX is the intervall time, 1 byte, e.g. 0x0A is 10 minutes
+//      RX:
+//          a) 0xD101 Success
+//          b) 0xD100 Fail
+
+import CoreBluetooth
+import Foundation
+import os.log
+import UIKit
+public enum MiaoMiaoResponseState: UInt8 {
+    case dataPacketReceived = 0x28
+    case newSensor = 0x32
+    case noSensor = 0x34
+    case frequencyChangedResponse = 0xD1
+}
+extension MiaoMiaoResponseState: CustomStringConvertible {
+    public var description: String {
+        switch self {
+        case .dataPacketReceived:
+            return "Data packet received"
+        case .newSensor:
+            return "New sensor detected"
+        case .noSensor:
+            return "No sensor found"
+        case .frequencyChangedResponse:
+            return "Reading intervall changed"
+        }
+    }
+}
+
+class MiaoMiaoTransmitter: LibreTransmitterProxyProtocol {
+
+    fileprivate lazy var logger = Logger(forType: Self.self)
+    
+    func reset() {
+        rxBuffer.resetAllBytes()
+    }
+
+    class var manufacturerer: String {
+        "Tomato"
+    }
+
+    class var smallImage: UIImage? {
+        UIImage(named: "miaomiao-small", in: Bundle.module, compatibleWith: nil)
+    }
+
+    class var shortTransmitterName: String {
+        "miaomiao"
+    }
+
+    class var requiresDelayedReconnect : Bool {
+        true
+    }
+
+    static var writeCharacteristic: UUIDContainer? = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
+    static var notifyCharacteristic: UUIDContainer? = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
+    static var serviceUUID: [UUIDContainer] = ["6E400001-B5A3-F393-E0A9-E50E24DCCA9E"]
+
+    weak var delegate: LibreTransmitterDelegate?
+
+    private var rxBuffer = Data()
+    private var sensorData: SensorData?
+    private var metadata: LibreTransmitterMetadata?
+
+
+
+    class func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool {
+        peripheral.name?.lowercased().starts(with: "miaomiao") ?? false
+    }
+
+    class func getDeviceDetailsFromAdvertisement(advertisementData: [String: Any]?) -> String? {
+        nil
+    }
+
+    required init(delegate: LibreTransmitterDelegate, advertisementData: [String: Any]?) {
+        //advertisementData is unknown for the miaomiao
+        self.delegate = delegate
+    }
+
+    func requestData(writeCharacteristics: CBCharacteristic, peripheral: CBPeripheral) {
+        confirmSensor(peripheral: peripheral, writeCharacteristics: writeCharacteristics)
+        reset()
+        logger.debug("dabear: miaomiaoRequestData")
+
+        peripheral.writeValue(Data([0xF0]), for: writeCharacteristics, type: .withResponse)
+    }
+
+    func updateValueForNotifyCharacteristics(_ value: Data, peripheral: CBPeripheral, writeCharacteristic: CBCharacteristic?) {
+        rxBuffer.append(value)
+
+        logger.debug("miaomiao Appended value with length  \(String(describing: value.count)), buffer length is: \(String(describing: self.rxBuffer.count))")
+
+
+
+        // When spreading a message over multiple telegrams, the miaomiao protocol
+        // does not repeat that initial byte
+        // firstbyte is therefore written to rxbuffer on first received telegram
+        // this becomes sort of a state to track which message is actually received.
+        // Therefore it also becomes important that once a message is fully received, the buffer is invalidated
+        //
+        guard let firstByte = rxBuffer.first, let miaoMiaoResponseState = MiaoMiaoResponseState(rawValue: firstByte) else {
+            reset()
+            logger.error("miaomiaoDidUpdateValueForNotifyCharacteristics did not undestand what to do (internal error")
+            return
+        }
+
+        switch miaoMiaoResponseState {
+        case .dataPacketReceived: // 0x28: // data received, append to buffer and inform delegate if end reached
+
+            if rxBuffer.count >= 363 {
+
+
+                delegate?.libreTransmitterReceivedMessage(0x0000, txFlags: 0x28, payloadData: rxBuffer)
+
+                handleCompleteMessage()
+                reset()
+            }
+
+        case .newSensor: // 0x32: // A new sensor has been detected -> acknowledge to use sensor and reset buffer
+            delegate?.libreTransmitterReceivedMessage(0x0000, txFlags: 0x32, payloadData: rxBuffer)
+
+            confirmSensor(peripheral: peripheral, writeCharacteristics: writeCharacteristic)
+            reset()
+        case .noSensor: // 0x34: // No sensor has been detected -> reset buffer (and wait for new data to arrive)
+
+            delegate?.libreTransmitterReceivedMessage(0x0000, txFlags: 0x34, payloadData: rxBuffer)
+
+            reset()
+        case .frequencyChangedResponse: // 0xD1: // Success of fail for setting time intervall
+
+            delegate?.libreTransmitterReceivedMessage(0x0000, txFlags: 0xD1, payloadData: rxBuffer)
+
+            if value.count >= 2 {
+                if value[2] == 0x01 {
+                    //success setting time interval
+                } else if value[2] == 0x00 {
+                    // faioure
+                } else {
+                    //"Unkown response for setting time interval."
+                }
+            }
+            reset()
+        }
+    }
+
+    func handleCompleteMessage() {
+        guard rxBuffer.count >= 363 else {
+            return
+        }
+
+        var patchInfo: String?
+
+        if rxBuffer.count >= 369 {
+            patchInfo = Data(rxBuffer[363...368]).hexEncodedString().uppercased()
+        }
+
+        logger.debug("rxbuffer length: \(self.rxBuffer.count ), patchinfo: \(String(describing: patchInfo))")
+
+        metadata = LibreTransmitterMetadata(
+            hardware: String(describing: rxBuffer[16...17].hexEncodedString()),
+            firmware: String(describing: rxBuffer[14...15].hexEncodedString()),
+            battery: Int(rxBuffer[13]),
+            name: Self.shortTransmitterName,
+            macAddress: nil,
+            patchInfo: patchInfo,
+            uid: [UInt8](rxBuffer[5..<13]) )
+
+        sensorData = SensorData(uuid: Data(rxBuffer.subdata(in: 5..<13)), bytes: [UInt8](rxBuffer.subdata(in: 18..<362)), date: Date())
+
+       if let sensorData = sensorData, let metadata = metadata {
+            delegate?.libreTransmitterDidUpdate(with: sensorData, and: metadata)
+        }
+    }
+
+    // Confirm (to replace) the sensor. Iif a new sensor is detected and shall be used, send this command (0xD301)
+    func confirmSensor(peripheral: CBPeripheral, writeCharacteristics: CBCharacteristic?) {
+        guard let writeCharacteristics = writeCharacteristics else {
+            logger.error("could not confirm sensor")
+            return
+        }
+        logger.debug("confirming new sensor")
+        peripheral.writeValue(Data([0xD3, 0x01]), for: writeCharacteristics, type: .withResponse)
+    }
+}

+ 20 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Bluetooth/Transmitter/UUIDContainer.swift

@@ -0,0 +1,20 @@
+//
+//  UUIDContainer.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 08/01/2020.
+//  Copyright © 2020 Bjørn Inge Berg. All rights reserved.
+//
+
+import CoreBluetooth
+import Foundation
+public struct UUIDContainer: ExpressibleByStringLiteral {
+    public var value: CBUUID
+
+    init(value: CBUUID) {
+        self.value = value
+    }
+    public init(stringLiteral value: String) {
+        self.value = CBUUID(string: value)
+    }
+}

+ 29 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/CollectionExtensions.swift

@@ -0,0 +1,29 @@
+//
+//  CollectionExtensions.swift
+//  MiaomiaoClientUI
+//
+//  Created by Bjørn Inge Berg on 26/03/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+extension Collection {
+    subscript(safe index: Index) -> Element? {
+        indices.contains(index) ? self[index] : nil
+    }
+}
+
+extension Array where Element: Hashable {
+    func removingDuplicates() -> [Element] {
+        var addedDict = [Element: Bool]()
+
+        return filter {
+            addedDict.updateValue(true, forKey: $0) == nil
+        }
+    }
+
+    mutating func removeDuplicates() {
+        self = self.removingDuplicates()
+    }
+}

+ 33 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/DataExtensions.swift

@@ -0,0 +1,33 @@
+//
+//  DataExtensions.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 22/09/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+extension Data {
+    mutating func resetAllBytes() {
+        self = Data()
+    }
+
+    // From Stackoverflow, see https://stackoverflow.com/questions/39075043/how-to-convert-data-to-hex-string-in-swift
+    private static let hexAlphabet = "0123456789abcdef".unicodeScalars.map { $0 }
+
+    public var hex: String {
+        return map { String(format: "%02X", $0) }.joined(separator: " ")
+    }
+
+    public func hexEncodedString() -> String {
+        String(self.reduce(into: "".unicodeScalars, { result, value in
+            result.append(Data.hexAlphabet[Int(value / 16)])
+            result.append(Data.hexAlphabet[Int(value % 16)])
+        }))
+    }
+
+    func toDebugString() -> String {
+           self.map { "\($0)" }.joined(separator: ", ")
+    }
+}

+ 97 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/DateExtensions.swift

@@ -0,0 +1,97 @@
+//
+//  DateExtensions.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 07/03/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+public extension Date {
+
+
+    func rounded(on amount: Int, _ component: Calendar.Component) -> Date {
+        let cal = Calendar.current
+        let value = cal.component(component, from: self)
+
+        // Compute nearest multiple of amount:
+        let roundedValue = lrint(Double(value) / Double(amount)) * amount
+        let newDate = cal.date(byAdding: component, value: roundedValue - value, to: self)!
+
+        return newDate.floorAllComponents(before: component)
+    }
+
+    func floorAllComponents(before component: Calendar.Component) -> Date {
+        // All components to round ordered by length
+        let components = [Calendar.Component.year, .month, .day, .hour, .minute, .second, .nanosecond]
+
+        guard let index = components.firstIndex(of: component) else {
+            fatalError("Wrong component")
+        }
+
+        let cal = Calendar.current
+        var date = self
+
+        components.suffix(from: index + 1).forEach { roundComponent in
+            let value = cal.component(roundComponent, from: date) * -1
+            date = cal.date(byAdding: roundComponent, value: value, to: date)!
+        }
+
+        return date
+    }
+
+    static var LocaleWantsAMPM: Bool {
+        DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: NSLocale.current)!.contains("a")
+    }
+
+    func getFormattedDate(format: String) -> String {
+        let dateformat = DateFormatter()
+        dateformat.dateFormat = format
+        return dateformat.string(from: self)
+    }
+}
+
+extension DateComponents {
+    func ToTimeString(wantsAMPM: Bool = Date.LocaleWantsAMPM) -> String {
+        //print("hour: \(self.hour) minute: \(self.minute)")
+        let date = Calendar.current.date(bySettingHour: self.hour ?? 0, minute: self.minute ?? 0, second: 0, of: Date())!
+
+        let formatter = DateFormatter()
+        formatter.dateStyle = DateFormatter.Style.long
+        formatter.timeStyle = DateFormatter.Style.medium
+
+        formatter.dateFormat = wantsAMPM ? "hh:mm a" : "HH:mm"
+        return formatter.string(from: date)
+    }
+}
+
+
+extension Array where Element == DateInterval {
+    // Check for intersection among the intervals in the given array and return
+    // the interval if found.
+    func intersect() -> DateInterval? {
+        // Algorithm:
+        // We will compare first two intervals.
+        // If an intersection is found, we will save the resultant interval
+        // and compare it with the next interval in the array.
+        // If no intersection is found at any iteration
+        // it means the intervals in the array are disjoint. Break the loop and return nil
+        // Otherwise return the last intersection.
+
+        var previous = self.first
+        for (index, element) in self.enumerated() {
+            if index == 0 {
+                continue
+            }
+
+            previous = previous?.intersection(with: element)
+
+            if previous == nil {
+                break
+            }
+        }
+
+        return previous
+    }
+}

+ 29 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/DoubleExtensions.swift

@@ -0,0 +1,29 @@
+//
+//  DoubleExtensions.swift
+//  MiaomiaoClientUI
+//
+//  Created by Bjørn Inge Berg on 25/03/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+extension Double {
+    func roundTo(places: Int) -> Double {
+        let divisor = pow(10.0, Double(places))
+        return (self * divisor).rounded() / divisor
+    }
+
+    var twoDecimals: String {
+        String(format: "%.2f", self)
+    }
+    var fourDecimals: String {
+        String(format: "%.4f", self)
+    }
+
+    enum Number {
+        static var formatter = NumberFormatter()
+    }
+
+
+}

+ 21 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/HashableClass.swift

@@ -0,0 +1,21 @@
+//
+//  HashableClass.swift
+//  LibreTransmitterUI
+//
+//  Created by Bjørn Inge Berg on 17/05/2021.
+//  Copyright © 2021 Mark Wilson. All rights reserved.
+//
+
+extension Hashable where Self: AnyObject {
+
+    public func hash(into hasher: inout Hasher) {
+        hasher.combine(ObjectIdentifier(self))
+    }
+}
+
+extension Equatable where Self: AnyObject {
+
+    public static func == (lhs:Self, rhs:Self) -> Bool {
+        return lhs === rhs
+    }
+}

+ 41 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Extensions/TimeIntervalExtensions.swift

@@ -0,0 +1,41 @@
+//
+//  NSTimeInterval.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 1/9/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+
+extension TimeInterval {
+    static func seconds(_ seconds: Double) -> TimeInterval {
+        seconds
+    }
+
+    static func minutes(_ minutes: Double) -> TimeInterval {
+        TimeInterval(minutes: minutes)
+    }
+
+    static func hours(_ hours: Double) -> TimeInterval {
+        TimeInterval(hours: hours)
+    }
+
+    init(minutes: Double) {
+        //self.init(minutes * 60)
+        let m = minutes * 60
+        self.init(m)
+    }
+
+    init(hours: Double) {
+        self.init(minutes: hours * 60)
+    }
+
+    var minutes: Double {
+        self / 60.0
+    }
+
+    var hours: Double {
+        minutes / 60.0
+    }
+}

+ 18 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/GlucoseSampleValue.swift

@@ -0,0 +1,18 @@
+//
+//  GlucoseSampleValue.swift
+//  LoopKit
+//
+//  Created by Nathan Racklyeft on 3/6/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+public protocol GlucoseSampleValue: GlucoseValue {
+    /// Uniquely identifies the source of the sample.
+    var provenanceIdentifier: String { get }
+
+    /// Whether the glucose value was provided for visual consistency, rather than an actual, calibrated reading.
+    var isDisplayOnly: Bool { get }
+
+    /// Whether the glucose value was entered by the user.
+    var wasUserEntered: Bool { get }
+}

+ 91 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/GlucoseValue.swift

@@ -0,0 +1,91 @@
+//
+//  GlucoseValue.swift
+//  LoopKit
+//
+//  Created by Nathan Racklyeft on 3/2/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+
+import HealthKit
+
+
+public protocol GlucoseValue: SampleValue {
+}
+
+public struct SimpleGlucoseValue: Equatable, GlucoseValue {
+    public let startDate: Date
+    public let endDate: Date
+    public let quantity: HKQuantity
+
+    public init(startDate: Date, endDate: Date? = nil, quantity: HKQuantity) {
+        self.startDate = startDate
+        self.endDate = endDate ?? startDate
+        self.quantity = quantity
+    }
+
+    public init(_ glucoseValue: GlucoseValue) {
+        self.startDate = glucoseValue.startDate
+        self.endDate = glucoseValue.endDate
+        self.quantity = glucoseValue.quantity
+    }
+}
+
+extension SimpleGlucoseValue: Codable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.startDate = try container.decode(Date.self, forKey: .startDate)
+        self.endDate = try container.decode(Date.self, forKey: .endDate)
+        self.quantity = HKQuantity(unit: HKUnit(from: try container.decode(String.self, forKey: .quantityUnit)),
+                                   doubleValue: try container.decode(Double.self, forKey: .quantity))
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(startDate, forKey: .startDate)
+        try container.encode(endDate, forKey: .endDate)
+        try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .quantity)
+        try container.encode(HKUnit.milligramsPerDeciliter.unitString, forKey: .quantityUnit)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case startDate
+        case endDate
+        case quantity
+        case quantityUnit
+    }
+}
+
+public struct PredictedGlucoseValue: Equatable, GlucoseValue {
+    public let startDate: Date
+    public let quantity: HKQuantity
+
+    public init(startDate: Date, quantity: HKQuantity) {
+        self.startDate = startDate
+        self.quantity = quantity
+    }
+}
+
+extension PredictedGlucoseValue: Codable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.startDate = try container.decode(Date.self, forKey: .startDate)
+        self.quantity = HKQuantity(unit: HKUnit(from: try container.decode(String.self, forKey: .quantityUnit)),
+                                   doubleValue: try container.decode(Double.self, forKey: .quantity))
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(startDate, forKey: .startDate)
+        try container.encode(quantity.doubleValue(for: .milligramsPerDeciliter), forKey: .quantity)
+        try container.encode(HKUnit.milligramsPerDeciliter.unitString, forKey: .quantityUnit)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case startDate
+        case quantity
+        case quantityUnit
+    }
+}
+
+extension HKQuantitySample: GlucoseValue { }

+ 27 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/HKQuantitySample+GlucoseKit.swift

@@ -0,0 +1,27 @@
+//
+//  GlucoseValue.swift
+//  LoopKit
+//
+//  Created by Nathan Racklyeft on 2/19/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import HealthKit
+
+
+let MetadataKeyGlucoseIsDisplayOnly = "com.loudnate.GlucoseKit.HKMetadataKey.GlucoseIsDisplayOnly"
+
+
+extension HKQuantitySample: GlucoseSampleValue {
+    public var provenanceIdentifier: String {
+        return sourceRevision.source.bundleIdentifier
+    }
+
+    public var isDisplayOnly: Bool {
+        return metadata?[MetadataKeyGlucoseIsDisplayOnly] as? Bool ?? false
+    }
+
+    public var wasUserEntered: Bool {
+        return metadata?[HKMetadataKeyWasUserEntered] as? Bool ?? false
+    }
+}

+ 67 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/HKUnit.swift

@@ -0,0 +1,67 @@
+//
+//  HKUnit.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 1/17/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import HealthKit
+
+
+extension HKUnit {
+    static let milligramsPerDeciliter: HKUnit = {
+        return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
+    }()
+
+    static let millimolesPerLiter: HKUnit = {
+        return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
+    }()
+
+    static let internationalUnitsPerHour: HKUnit = {
+        return HKUnit.internationalUnit().unitDivided(by: .hour())
+    }()
+
+    static let gramsPerUnit: HKUnit = {
+        return 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)
+        }
+    }
+}

+ 29 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/LocalizedString.swift

@@ -0,0 +1,29 @@
+//
+//  LocalizedString.swift
+//  LoopKit
+//
+//  Created by Retina15 on 8/6/18.
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+// really needs to be a class to compile 
+// swiftlint:disable:next convenience_type
+internal class FrameworkBundle {
+    static let main = Bundle(for: FrameworkBundle.self)
+}
+
+func LocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String {
+    if let value = value {
+        return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, value: value, comment: comment)
+    } else {
+        return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, comment: comment)
+    }
+}
+/*
+extension DefaultStringInterpolation {
+    mutating func appendInterpolation<T>(_ optional: T?) {
+        appendInterpolation(String(describing: optional))
+    }
+}*/

+ 48 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/MessagePassing.swift

@@ -0,0 +1,48 @@
+//
+//  MessagePassing.swift
+//  LibreTransmitter
+//
+//  Created by Bjørn Inge Berg on 21/04/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+public func bundleSeedID() -> String? {
+    let queryLoad: [String: AnyObject] = [
+        kSecClass as String: kSecClassGenericPassword,
+        kSecAttrAccount as String: "bundleSeedID" as AnyObject,
+        kSecAttrService as String: "" as AnyObject,
+        kSecReturnAttributes as String: kCFBooleanTrue
+    ]
+
+    var result: AnyObject?
+    var status = withUnsafeMutablePointer(to: &result) {
+        SecItemCopyMatching(queryLoad as CFDictionary, UnsafeMutablePointer($0))
+    }
+
+    if status == errSecItemNotFound {
+        status = withUnsafeMutablePointer(to: &result) {
+            SecItemAdd(queryLoad as CFDictionary, UnsafeMutablePointer($0))
+        }
+    }
+
+    if status == noErr {
+        if let resultDict = result as? [String: Any], let accessGroup = resultDict[kSecAttrAccessGroup as String] as? String {
+            let components = accessGroup.components(separatedBy: ".")
+            return components.first
+        } else {
+            return nil
+        }
+    } else {
+        print("Error getting bundleSeedID to Keychain")
+        return nil
+    }
+}
+
+public func getDynamicAppGroupForMessagePassing() -> String? {
+    if let seed = bundleSeedID() {
+        return "group.com.\(seed).Loopkit.Loop.MessagePassing"
+    }
+    return nil
+}

+ 63 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NewGlucoseSample.swift

@@ -0,0 +1,63 @@
+//
+//  NewGlucoseSample.swift
+//  LoopKit
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import HealthKit
+
+
+public struct NewGlucoseSample: Equatable {
+    public let date: Date
+    public let quantity: HKQuantity
+    public let isDisplayOnly: Bool
+    public let wasUserEntered: Bool
+    public let syncIdentifier: String
+    public var syncVersion: Int
+    public var device: HKDevice?
+
+    /// - Parameters:
+    ///   - date: The date the sample was collected
+    ///   - quantity: The glucose sample quantity
+    ///   - isDisplayOnly: Whether the reading was shifted for visual consistency after calibration
+    ///   - wasUserEntered: Whether the reading was entered by the user (manual) or not (device)
+    ///   - syncIdentifier: A unique identifier representing the sample, used for de-duplication
+    ///   - syncVersion: A version number for determining resolution in de-duplication
+    ///   - device: The description of the device the collected the sample
+    public init(date: Date, quantity: HKQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, syncIdentifier: String, syncVersion: Int = 1, device: HKDevice? = nil) {
+        self.date = date
+        self.quantity = quantity
+        self.isDisplayOnly = isDisplayOnly
+        self.wasUserEntered = wasUserEntered
+        self.syncIdentifier = syncIdentifier
+        self.syncVersion = syncVersion
+        self.device = device
+    }
+}
+
+
+extension NewGlucoseSample {
+    public var quantitySample: HKQuantitySample {
+        var metadata: [String: Any] = [
+            HKMetadataKeySyncIdentifier: syncIdentifier,
+            HKMetadataKeySyncVersion: syncVersion,
+        ]
+
+        if isDisplayOnly {
+            metadata[MetadataKeyGlucoseIsDisplayOnly] = true
+        }
+        if wasUserEntered {
+            metadata[HKMetadataKeyWasUserEntered] = true
+        }
+
+        return HKQuantitySample(
+            type: HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!,
+            quantity: quantity,
+            start: date,
+            end: date,
+            device: device,
+            metadata: metadata
+        )
+    }
+}

+ 447 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NotificationHelper.swift

@@ -0,0 +1,447 @@
+//
+//  NotificationHelper.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 30/05/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import AudioToolbox
+import Foundation
+import HealthKit
+import UserNotifications
+import os.log
+import UIKit
+import AudioToolbox
+
+fileprivate var logger = Logger(forType: "NotificationHelper")
+
+public enum NotificationHelper {
+
+    private enum Identifiers: String {
+        case glucocoseNotifications = "no.bjorninge.miaomiao.glucose-notification"
+        case noSensorDetected = "no.bjorninge.miaomiao.nosensordetected-notification"
+        case tryAgainLater = "no.bjorninge.miaomiao.glucoseNotAvailableTryAgainLater-notification"
+        case sensorChange = "no.bjorninge.miaomiao.sensorchange-notification"
+        case invalidSensor = "no.bjorninge.miaomiao.invalidsensor-notification"
+        case lowBattery = "no.bjorninge.miaomiao.lowbattery-notification"
+        case sensorExpire = "no.bjorninge.miaomiao.SensorExpire-notification"
+        case noBridgeSelected = "no.bjorninge.miaomiao.noBridgeSelected-notification"
+        case bluetoothPoweredOff = "no.bjorninge.miaomiao.bluetoothPoweredOff-notification"
+        case invalidChecksum = "no.bjorninge.miaomiao.invalidChecksum-notification"
+        case calibrationOngoing = "no.bjorninge.miaomiao.calibration-notification"
+        case restoredState = "no.bjorninge.miaomiao.state-notification"
+    }
+
+    public static func playSoundIfNeeded(count: Int = 3) {
+        if UserDefaults.standard.mmGlucoseAlarmsVibrate {
+            playSound(times: count)
+        }
+    }
+
+    private static func playSound(times: Int) {
+        guard times > 0 else {
+            return
+        }
+
+        AudioServicesPlaySystemSoundWithCompletion(SystemSoundID(1336)) {
+            playSound(times: times - 1)
+        }
+    }
+
+    public static func GlucoseUnitIsSupported(unit: HKUnit) -> Bool {
+        [HKUnit.milligramsPerDeciliter, HKUnit.millimolesPerLiter].contains(unit)
+    }
+
+    public static func sendRestoredStateNotification(msg: String) {
+        ensureCanSendNotification {
+            logger.debug("dabear:: sending RestoredStateNotification")
+
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("State was restored", comment: "State was restored")
+            content.body = msg
+
+            addRequest(identifier: .restoredState, content: content )
+        }
+    }
+
+    public static func sendBluetoothPowerOffNotification() {
+        ensureCanSendNotification {
+            logger.debug("dabear:: sending BluetoothPowerOffNotification")
+
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("Bluetooth Power Off", comment: "Bluetooth Power Off")
+            content.body = NSLocalizedString("Please turn on Bluetooth", comment: "Please turn on Bluetooth")
+
+            addRequest(identifier: .bluetoothPoweredOff, content: content)
+        }
+    }
+
+    public static func sendNoTransmitterSelectedNotification() {
+        ensureCanSendNotification {
+            logger.debug("dabear:: sending NoTransmitterSelectedNotification")
+
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("No Libre Transmitter Selected", comment: "No Libre Transmitter Selected")
+            content.body = NSLocalizedString("Delete Transmitter and start anew.", comment: "Delete Transmitter and start anew.")
+
+            addRequest(identifier: .noBridgeSelected, content: content)
+        }
+    }
+
+    private static func ensureCanSendGlucoseNotification(_ completion: @escaping (_ unit: HKUnit) -> Void ) {
+        ensureCanSendNotification {
+            if let glucoseUnit = UserDefaults.standard.mmGlucoseUnit, GlucoseUnitIsSupported(unit: glucoseUnit) {
+                completion(glucoseUnit)
+            }
+        }
+    }
+
+    public static func requestNotificationPermissionsIfNeeded(){
+        UNUserNotificationCenter.current().getNotificationSettings { settings in
+            logger.debug("settings.authorizationStatus: \(String(describing: settings.authorizationStatus.rawValue))")
+            if ![.authorized,.provisional].contains(settings.authorizationStatus) {
+                requestNotificationPermissions()
+            }
+
+        }
+
+    }
+
+    private static func requestNotificationPermissions() {
+        logger.debug("requestNotificationPermissions called")
+        let center = UNUserNotificationCenter.current()
+        center.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
+            if granted {
+                logger.debug("requestNotificationPermissions was granted")
+            } else {
+                logger.debug("requestNotificationPermissions failed because of error: \(String(describing: error))")
+            }
+
+        }
+
+
+    }
+
+    private static func ensureCanSendNotification(_ completion: @escaping () -> Void ) {
+        UNUserNotificationCenter.current().getNotificationSettings { settings in
+            guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
+                logger.debug("dabear:: ensureCanSendNotification failed, authorization denied")
+                return
+            }
+
+            logger.debug("dabear:: sending notification was allowed")
+
+            completion()
+        }
+    }
+
+    public static func sendInvalidChecksumIfDeveloper(_ sensorData: SensorData) {
+        if sensorData.hasValidCRCs {
+            return
+        }
+
+        ensureCanSendNotification {
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("Invalid libre checksum", comment: "Invalid libre checksum")
+            content.body = NSLocalizedString("Libre sensor was incorrectly read, CRCs were not valid", comment: "Libre sensor was incorrectly read, CRCs were not valid")
+
+            addRequest(identifier: .invalidChecksum, content: content)
+        }
+    }
+
+    private static var glucoseNotifyCalledCount = 0
+
+    public static func sendGlucoseNotitifcationIfNeeded(glucose: LibreGlucose, oldValue: LibreGlucose?, trend: GlucoseTrend?, battery: String?) {
+        glucoseNotifyCalledCount &+= 1
+
+        let shouldSendGlucoseAlternatingTimes = glucoseNotifyCalledCount != 0 && UserDefaults.standard.mmNotifyEveryXTimes != 0
+
+        let shouldSend = UserDefaults.standard.mmAlwaysDisplayGlucose || glucoseNotifyCalledCount == 1 || (shouldSendGlucoseAlternatingTimes && glucoseNotifyCalledCount % UserDefaults.standard.mmNotifyEveryXTimes == 0)
+
+        let schedules = UserDefaults.standard.glucoseSchedules
+
+        let alarm = schedules?.getActiveAlarms(glucose.glucoseDouble) ?? .none
+        let isSnoozed = GlucoseScheduleList.isSnoozed()
+
+        let transmitterBattery = UserDefaults.standard.mmShowTransmitterBattery && battery != nil ? battery : nil
+
+        logger.debug("dabear:: glucose alarmtype is \(String(describing:alarm))")
+        // We always send glucose notifications when alarm is active,
+        // even if glucose notifications are disabled in the UI
+
+        if shouldSend || alarm.isAlarming() {
+            sendGlucoseNotitifcation(glucose: glucose, oldValue: oldValue, alarm: alarm, isSnoozed: isSnoozed, trend: trend, transmitterBattery: transmitterBattery)
+        } else {
+            logger.debug("dabear:: not sending glucose, shouldSend and alarmIsActive was false")
+            return
+        }
+    }
+
+    private static func addRequest(identifier: Identifiers, content: UNMutableNotificationContent, deleteOld: Bool = false) {
+        let center = UNUserNotificationCenter.current()
+        //content.sound = UNNotificationSound.
+        let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: nil)
+
+        if deleteOld {
+            // Required since ios12+ have started to cache/group notifications
+            center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
+            center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
+        }
+
+        center.add(request) { error in
+            if let error = error {
+                logger.debug("dabear:: unable to addNotificationRequest: \(error.localizedDescription)")
+                return
+            }
+
+            logger.debug("dabear:: sending \(identifier.rawValue) notification")
+        }
+    }
+    private static func sendGlucoseNotitifcation(glucose: LibreGlucose, oldValue: LibreGlucose?, alarm: GlucoseScheduleAlarmResult = .none, isSnoozed: Bool = false, trend: GlucoseTrend?, transmitterBattery: String?) {
+        ensureCanSendGlucoseNotification { _ in
+            let content = UNMutableNotificationContent()
+            let glucoseDesc = glucose.description
+            var titles = [String]()
+            var body = [String]()
+            var body2 = [String]()
+            switch alarm {
+            case .none:
+                titles.append(LocalizedString("Glucose", comment: "Glucose"))
+            case .low:
+                titles.append(LocalizedString("LOWALERT!", comment: "LOWALERT!"))
+            case .high:
+                titles.append(LocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
+            }
+
+            if isSnoozed {
+                titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
+            } else if alarm.isAlarming() {
+                content.sound = .default
+                playSoundIfNeeded()
+            }
+            titles.append(glucoseDesc)
+
+            body.append(String(format: NSLocalizedString("Glucose: %@", comment: "Glucose: %@"), glucoseDesc))
+
+            if let oldValue = oldValue {
+                body.append( LibreGlucose.glucoseDiffDesc(oldValue: oldValue, newValue: glucose))
+            }
+
+            if let trendSymbol = trend?.symbol {
+                body.append("\(trendSymbol)")
+            }
+
+            if let transmitterBattery = transmitterBattery {
+                body2.append(String(format: NSLocalizedString("Transmitter: %@%%", comment: "Transmitter: %@%%"), transmitterBattery))
+            }
+
+            //these are texts that naturally fit on their own line in the body
+            var body2s = ""
+            if !body2.isEmpty {
+                body2s = "\n" + body2.joined(separator: "\n")
+            }
+
+            content.title = titles.joined(separator: " ")
+            content.body = body.joined(separator: ", ") + body2s
+            addRequest(identifier: .glucocoseNotifications,
+                       content: content,
+                       deleteOld: true)
+        }
+    }
+
+    public enum CalibrationMessage: String {
+        case starting = "Calibrating sensor, please stand by!"
+        case noCalibration = "Could not calibrate sensor, check libreoopweb permissions and internet connection"
+        case invalidCalibrationData = "Could not calibrate sensor, invalid calibrationdata"
+        case success = "Success!"
+    }
+
+    public static func sendCalibrationNotification(_ calibrationMessage: CalibrationMessage) {
+        ensureCanSendNotification {
+            let content = UNMutableNotificationContent()
+            content.sound = .default
+            content.title = NSLocalizedString("Extracting calibrationdata from sensor", comment: "Extracting calibrationdata from sensor")
+            content.body = NSLocalizedString(calibrationMessage.rawValue, comment: "calibrationMessage")
+
+            addRequest(identifier: .calibrationOngoing,
+                       content: content,
+                       deleteOld: true)
+        }
+    }
+
+    public static func sendSensorNotDetectedNotificationIfNeeded(noSensor: Bool) {
+        guard UserDefaults.standard.mmAlertNoSensorDetected && noSensor else {
+            logger.debug("Not sending noSensorDetected notification")
+            return
+        }
+
+        sendSensorNotDetectedNotification()
+    }
+
+    private static func sendSensorNotDetectedNotification() {
+        ensureCanSendNotification {
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("No Sensor Detected", comment: "No Sensor Detected")
+            content.body = NSLocalizedString("This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor", comment: "This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor")
+
+            addRequest(identifier: .noSensorDetected, content: content)
+        }
+    }
+
+    public static func sendSensorChangeNotificationIfNeeded() {
+        guard UserDefaults.standard.mmAlertNewSensorDetected else {
+            logger.debug("not sending sendSensorChange notification ")
+            return
+        }
+        sendSensorChangeNotification()
+    }
+
+    private static func sendSensorChangeNotification() {
+        ensureCanSendNotification {
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("New Sensor Detected", comment: "New Sensor Detected")
+            content.body = NSLocalizedString("Please wait up to 30 minutes before glucose readings are available!", comment: "Please wait up to 30 minutes before glucose readings are available!")
+
+            addRequest(identifier: .sensorChange, content: content)
+            //content.sound = UNNotificationSound.
+
+        }
+    }
+
+    public static func sendSensorTryAgainLaterNotification() {
+        ensureCanSendNotification {
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("Invalid Glucose sample detected, try again later", comment: "Invalid Glucose sample detected, try again later")
+            content.body = NSLocalizedString("Sensor might have temporarily stopped, fallen off or is too cold or too warm", comment: "Sensor might have temporarily stopped, fallen off or is too cold or too warm")
+
+            addRequest(identifier: .tryAgainLater, content: content)
+            //content.sound = UNNotificationSound.
+
+        }
+    }
+
+
+
+    public static func sendInvalidSensorNotificationIfNeeded(sensorData: SensorData) {
+        let isValid = sensorData.isLikelyLibre1FRAM && (sensorData.state == .starting || sensorData.state == .ready)
+
+        guard UserDefaults.standard.mmAlertInvalidSensorDetected && !isValid else {
+            logger.debug("not sending invalidSensorDetected notification")
+            return
+        }
+
+        sendInvalidSensorNotification(sensorData: sensorData)
+    }
+
+    private static func sendInvalidSensorNotification(sensorData: SensorData) {
+        ensureCanSendNotification {
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("Invalid Sensor Detected", comment: "Invalid Sensor Detected")
+
+            if !sensorData.isLikelyLibre1FRAM {
+                content.body = NSLocalizedString("Detected sensor seems not to be a libre 1 sensor!", comment: "Detected sensor seems not to be a libre 1 sensor!")
+            } else if !(sensorData.state == .starting || sensorData.state == .ready) {
+                content.body = String(format: NSLocalizedString("Detected sensor is invalid: %@", comment: "Detected sensor is invalid: %@"), sensorData.state.description)
+            }
+
+            content.sound = .default
+
+            addRequest(identifier: .invalidSensor, content: content)
+        }
+    }
+
+    private static var lastBatteryWarning: Date?
+
+    public static func sendLowBatteryNotificationIfNeeded(device: LibreTransmitterMetadata) {
+        guard UserDefaults.standard.mmAlertLowBatteryWarning else {
+            logger.debug("mmAlertLowBatteryWarning toggle was not enabled, not sending low notification")
+            return
+        }
+
+        if let battery = device.battery, battery > 20 {
+            logger.debug("device battery is \(battery), not sending low notification")
+            return
+
+        }
+
+        let now = Date()
+        //only once per mins minute
+        let mins = 60.0 * 120
+        if let earlierplus = lastBatteryWarning?.addingTimeInterval(mins) {
+            if earlierplus < now {
+                sendLowBatteryNotification(batteryPercentage: device.batteryString,
+                                           deviceName: device.name)
+                lastBatteryWarning = now
+            } else {
+                logger.debug("Device battery is running low, but lastBatteryWarning Notification was sent less than 45 minutes ago, aborting. earlierplus: \(earlierplus), now: \(now)")
+            }
+        } else {
+            sendLowBatteryNotification(batteryPercentage: device.batteryString,
+                                       deviceName: device.name)
+            lastBatteryWarning = now
+        }
+    }
+
+    private static func sendLowBatteryNotification(batteryPercentage: String, deviceName: String) {
+        ensureCanSendNotification {
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("Low Battery", comment: "Low Battery")
+            content.body = String(format: NSLocalizedString("Battery is running low %@, consider charging your %@ device as soon as possible", comment: ""), batteryPercentage, deviceName)
+
+
+            content.sound = .default
+
+            addRequest(identifier: .lowBattery, content: content)
+        }
+    }
+
+    private static var lastSensorExpireAlert: Date?
+
+    public static func sendSensorExpireAlertIfNeeded(minutesLeft: Double) {
+        guard UserDefaults.standard.mmAlertWillSoonExpire else {
+            logger.debug("mmAlertWillSoonExpire toggle was not enabled, not sending expiresoon alarm")
+            return
+        }
+
+        guard TimeInterval(minutes: minutesLeft) < TimeInterval(hours: 24) else {
+            logger.debug("Sensor time left was more than 24 hours, not sending notification: \(minutesLeft.twoDecimals) minutes")
+            return
+        }
+
+        let now = Date()
+        //only once per 6 hours
+        let min45 = 60.0 * 60 * 6
+
+        if let earlier = lastSensorExpireAlert {
+            if earlier.addingTimeInterval(min45) < now {
+                sendSensorExpireAlert(minutesLeft: minutesLeft)
+                lastSensorExpireAlert = now
+            } else {
+                logger.debug("Sensor is soon expiring, but lastSensorExpireAlert was sent less than 6 hours ago, so aborting")
+            }
+        } else {
+            sendSensorExpireAlert(minutesLeft: minutesLeft)
+            lastSensorExpireAlert = now
+        }
+    }
+
+    public static func sendSensorExpireAlertIfNeeded(sensorData: SensorData) {
+        sendSensorExpireAlertIfNeeded(minutesLeft: Double(sensorData.minutesLeft))
+    }
+
+    private static func sendSensorExpireAlert(minutesLeft: Double) {
+        ensureCanSendNotification {
+
+            let hours = minutesLeft == 0 ? 0 : round(minutesLeft/60)
+
+            let dynamicText =  hours <= 1 ?  "minutes: \(minutesLeft.twoDecimals)" : "hours: \(hours.twoDecimals)"
+
+            let content = UNMutableNotificationContent()
+            content.title = NSLocalizedString("Sensor Ending Soon", comment: "Sensor Ending Soon")
+            content.body = String(format: NSLocalizedString("Current Sensor is Ending soon! Sensor Life left in %@", comment: ""), dynamicText)
+
+            addRequest(identifier: .sensorExpire, content: content, deleteOld: true)
+        }
+    }
+}

+ 47 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NumberFormatter.swift

@@ -0,0 +1,47 @@
+//
+//  NSNumberFormatter.swift
+//  Loop
+//
+//  Created by Nate Racklyeft on 9/5/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+extension NumberFormatter {
+    func string(from number: Double) -> String? {
+        return string(from: NSNumber(value: number))
+    }
+
+    func string(from number: Double, unit: String, style: Formatter.UnitStyle = .medium) -> String? {
+        guard let stringValue = string(from: number) else {
+            return nil
+        }
+
+        let format: String
+        switch style {
+        case .long, .medium:
+            format = LocalizedString(
+                "quantity-and-unit-space",
+                value: "%1$@ %2$@",
+                comment: "Format string for combining localized numeric value and unit with a space. (1: numeric value)(2: unit)"
+            )
+        case .short:
+            fallthrough
+        @unknown default:
+            format = LocalizedString(
+                "quantity-and-unit-tight",
+                value: "%1$@%2$@",
+                comment: "Format string for combining localized numeric value and unit without spacing. (1: numeric value)(2: unit)"
+            )
+        }
+
+        return String(
+            format: format,
+            stringValue,
+            unit
+        )
+    }
+}

+ 252 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/QuantityFormatter.swift

@@ -0,0 +1,252 @@
+//
+//  QuantityFormatter.swift
+//  LoopKit
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+/// Formats unit quantities as localized strings
+open class QuantityFormatter {
+
+    public init() {
+    }
+
+    public convenience init(for unit: HKUnit) {
+        self.init()
+        setPreferredNumberFormatter(for: unit)
+    }
+
+    /// The unit style determines how the unit strings are abbreviated, and spacing between the value and unit
+    open var unitStyle: Formatter.UnitStyle = .medium {
+        didSet {
+            if hasMeasurementFormatter {
+                measurementFormatter.unitStyle = unitStyle
+            }
+
+            if hasMassFormatter {
+                massFormatter.unitStyle = unitStyle
+            }
+        }
+    }
+
+    open var locale: Locale = Locale.current {
+        didSet {
+            if hasNumberFormatter {
+                numberFormatter.locale = locale
+            }
+
+            if hasMeasurementFormatter {
+                measurementFormatter.locale = locale
+            }
+        }
+    }
+
+    /// Updates `numberFormatter` configuration for the specified unit
+    ///
+    /// - Parameter unit: The unit
+    open func setPreferredNumberFormatter(for unit: HKUnit) {
+        numberFormatter.numberStyle = .decimal
+        numberFormatter.minimumFractionDigits = unit.preferredFractionDigits
+        numberFormatter.maximumFractionDigits = unit.maxFractionDigits
+    }
+
+    private var hasNumberFormatter = false
+
+    /// The formatter used for the quantity values
+    open private(set) lazy var numberFormatter: NumberFormatter = {
+        hasNumberFormatter = true
+
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.locale = self.locale
+        return formatter
+    }()
+
+    private var hasMeasurementFormatter = false
+
+    /// MeasurementFormatter is used for gram measurements, mg/dL units, and mmol/L units.
+    /// It does not properly handle glucose measurements, as it changes unit scales: 100 mg/dL -> 1 g/L
+    private lazy var measurementFormatter: MeasurementFormatter = {
+        hasMeasurementFormatter = true
+
+        let formatter = MeasurementFormatter()
+        formatter.unitOptions = [.providedUnit]
+        formatter.numberFormatter = self.numberFormatter
+        formatter.locale = self.locale
+        formatter.unitStyle = self.unitStyle
+        return formatter
+    }()
+
+    private var hasMassFormatter = false
+
+    /// MassFormatter properly creates unit strings for grams in .short/.medium style as "g", where MeasurementFormatter uses "gram"/"grams"
+    private lazy var massFormatter: MassFormatter = {
+        hasMassFormatter = true
+
+        let formatter = MassFormatter()
+        formatter.numberFormatter = self.numberFormatter
+        formatter.unitStyle = self.unitStyle
+        return formatter
+    }()
+
+    /// Formats a quantity and unit as a localized string
+    ///
+    /// - Parameters:
+    ///   - quantity: The quantity
+    ///   - unit: The unit. An exception is thrown if `quantity` is not compatible with the unit.
+    ///   - includeUnit: Whether or not to include the unit in the returned string
+    /// - Returns: A localized string, or nil if `numberFormatter` is unable to format the quantity value
+    open func string(from quantity: HKQuantity, for unit: HKUnit, includeUnit: Bool = true) -> String? {
+        let value = quantity.doubleValue(for: unit)
+
+        if !includeUnit {
+            return numberFormatter.string(from: value)
+        }
+
+        if let foundationUnit = unit.foundationUnit, unit.usesMeasurementFormatterForMeasurement {
+            return measurementFormatter.string(from: Foundation.Measurement(value: value, unit: foundationUnit))
+        }        
+        
+        return numberFormatter.string(from: value, unit: string(from: unit, forValue: value), style: unitStyle)
+    }
+
+    /// Formats a unit as a localized string
+    ///
+    /// - Parameters:
+    ///   - unit: The unit
+    ///   - value: An optional value for determining the plurality of the unit string
+    /// - Returns: A string for the unit. If no localization entry is available, the unlocalized `unitString` is returned.
+    open func string(from unit: HKUnit, forValue value: Double = 10) -> String {
+        if let string = unit.localizedUnitString(in: unitStyle, singular: abs(1.0 - value) < .ulpOfOne) {
+            return string
+        }
+
+        if unit.usesMassFormatterForUnitString {
+            return massFormatter.unitString(fromValue: value, unit: HKUnit.massFormatterUnit(from: unit))
+        }
+
+        if let foundationUnit = unit.foundationUnit {
+            return measurementFormatter.string(from: foundationUnit)
+        }
+
+        // Fallback, unlocalized
+        return unit.unitString
+    }
+}
+
+
+public extension HKUnit {
+    var usesMassFormatterForUnitString: Bool {
+        return self == .gram()
+    }
+
+    var usesMeasurementFormatterForMeasurement: Bool {
+        return self == .gram()
+    }
+
+    var preferredFractionDigits: Int {
+        if self == HKUnit.millimolesPerLiter || self == HKUnit.millimolesPerLiter.unitDivided(by: .internationalUnit()) {
+            return 1
+        } else {
+            return 0
+        }
+    }
+
+    var maxFractionDigits: Int {
+        switch self {
+        case .internationalUnit(), .internationalUnitsPerHour:
+            return 3
+        case HKUnit.gram().unitDivided(by: .internationalUnit()):
+            return 2
+        default:
+            return preferredFractionDigits
+        }
+    }
+
+    // Short localized unit string with unlocalized fallback
+    func shortLocalizedUnitString() -> String {
+        return localizedUnitString(in: .short) ?? unitString
+    }
+
+    func localizedUnitString(in style: Formatter.UnitStyle, singular: Bool = false) -> String? {
+        if self == .internationalUnit() {
+            switch style {
+            case .short, .medium:
+                return LocalizedString("U", comment: "The short unit display string for international units of insulin")
+            case .long:
+                fallthrough
+            @unknown default:
+                if singular {
+                    return LocalizedString("Unit", comment: "The long unit display string for a singular international unit of insulin")
+                } else {
+                    return LocalizedString("Units", comment: "The long unit display string for international units of insulin")
+                }
+            }
+        }
+
+        if self == .internationalUnitsPerHour {
+            switch style {
+            case .short, .medium:
+                return LocalizedString("U/hr", comment: "The short unit display string for international units of insulin per hour")
+            case .long:
+                fallthrough
+            @unknown default:
+                if singular {
+                    return LocalizedString("Unit/hour", comment: "The long unit display string for a singular international unit of insulin per hour")
+                } else {
+                    return LocalizedString("Units/hour", comment: "The long unit display string for international units of insulin per hour")
+                }
+            }
+        }
+
+        if self == HKUnit.millimolesPerLiter {
+            switch style {
+            case .short, .medium:
+                return LocalizedString("mmol/L", comment: "The short unit display string for millimoles per liter")
+            case .long:
+                break  // Fallback to the MeasurementFormatter localization
+            @unknown default:
+                break
+            }
+        }
+
+        if self == HKUnit.milligramsPerDeciliter.unitDivided(by: HKUnit.internationalUnit()) {
+            switch style {
+            case .short, .medium:
+                return LocalizedString("mg/dL/U", comment: "The short unit display string for milligrams per deciliter per U")
+            case .long:
+                break  // Fallback to the MeasurementFormatter localization
+            @unknown default:
+                break
+            }
+        }
+
+        if self == HKUnit.millimolesPerLiter.unitDivided(by: HKUnit.internationalUnit()) {
+            switch style {
+            case .short, .medium:
+                return LocalizedString("mmol/L/U", comment: "The short unit display string for millimoles per liter per U")
+            case .long:
+                break  // Fallback to the MeasurementFormatter localization
+            @unknown default:
+                break
+            }
+        }
+
+        if self == HKUnit.gram().unitDivided(by: HKUnit.internationalUnit()) {
+            switch style {
+            case .short, .medium:
+                return LocalizedString("g/U", comment: "The short unit display string for grams per U")
+            case .long:
+                fallthrough
+            @unknown default:
+                break  // Fallback to the MeasurementFormatter localization
+            }
+        }
+
+        return nil
+    }
+}

+ 115 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/SampleValue.swift

@@ -0,0 +1,115 @@
+//
+//  SampleValue.swift
+//  Naterade
+//
+//  Created by Nathan Racklyeft on 1/24/16.
+//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+
+public protocol TimelineValue {
+    var startDate: Date { get }
+    var endDate: Date { get }
+}
+
+
+public extension TimelineValue {
+    var endDate: Date {
+        return startDate
+    }
+}
+
+
+public protocol SampleValue: TimelineValue {
+    var quantity: HKQuantity { get }
+}
+
+
+public extension Sequence where Element: TimelineValue {
+    /**
+     Returns the closest element in the sorted sequence prior to the specified date
+
+     - parameter date: The date to use in the search
+
+     - returns: The closest element, if any exist before the specified date
+     */
+    func closestPrior(to date: Date) -> Iterator.Element? {
+        return elementsAdjacent(to: date).before
+    }
+
+    /// Returns the elements immediately before and after the specified date
+    ///
+    /// - Parameter date: The date to use in the search
+    /// - Returns: The closest elements, if found
+    func elementsAdjacent(to date: Date) -> (before: Iterator.Element?, after: Iterator.Element?) {
+        var before: Iterator.Element?
+        var after: Iterator.Element?
+
+        for value in self {
+            if value.startDate <= date {
+                before = value
+            } else {
+                after = value
+                break
+            }
+        }
+
+        return (before, after)
+    }
+
+    /// Returns all elements inmmediately adjacent to the specified date
+    ///
+    /// Use Sequence.elementsAdjacent(to:) if specific before/after references are necessary
+    ///
+    /// - Parameter date: The date to use in the search
+    /// - Returns: The closest elements, if found
+    func allElementsAdjacent(to date: Date) -> [Iterator.Element] {
+        let (before, after) = elementsAdjacent(to: date)
+        return [before, after].compactMap({ $0 })
+    }
+
+    /**
+     Returns an array of elements filtered by the specified date range.
+     
+     This behavior mimics HKQueryOptionNone, where the value must merely overlap the specified range,
+     not strictly exist inside of it.
+
+     - parameter startDate: The earliest date of elements to return
+     - parameter endDate:   The latest date of elements to return
+
+     - returns: A new array of elements
+     */
+    func filterDateRange(_ startDate: Date?, _ endDate: Date?) -> [Iterator.Element] {
+        return filter { (value) -> Bool in
+            if let startDate = startDate, value.endDate < startDate {
+                return false
+            }
+
+            if let endDate = endDate, value.startDate > endDate {
+                return false
+            }
+
+            return true
+        }
+    }
+}
+
+public extension Sequence where Element: SampleValue {
+    func average(unit: HKUnit) -> HKQuantity? {
+        let (sum, count) = reduce(into: (sum: 0.0, count: 0)) { result, element in
+            result.0 += element.quantity.doubleValue(for: unit)
+            result.1 += 1
+        }
+        
+        guard count > 0 else {
+            return nil
+        }
+        
+        let average = sum / Double(count)
+        
+        return HKQuantity(unit: unit, doubleValue: average)
+    }
+}

+ 226 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/GlucoseSchedules.swift

@@ -0,0 +1,226 @@
+//
+//  GlucoseSchedules.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 19/04/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+//import MiaomiaoClient
+public enum GlucoseScheduleAlarmResult: Int, CaseIterable {
+    case none = 0
+    case low
+    case high
+
+    func isAlarming() -> Bool {
+        rawValue != GlucoseScheduleAlarmResult.none.rawValue
+    }
+}
+
+enum GlucoseSchedulesValidationStatus {
+    case success
+    case error(String)
+}
+
+class GlucoseScheduleList: Codable, CustomStringConvertible {
+    var description: String {
+        "(schedules: \(schedules) )"
+    }
+
+    public var schedules = [GlucoseSchedule]()
+
+    public var enabledSchedules: [GlucoseSchedule] {
+        schedules.compactMap({ $0.enabled == true ? $0 : nil })
+    }
+
+    //this is only used by the ui to count total number of schedules
+    public static let minimumSchedulesCount = 2
+
+    public var activeSchedules: [GlucoseSchedule] {
+        enabledSchedules.compactMap {
+            if let activeTime = $0.getScheduleActiveToFrom() {
+                let now = Date()
+                return activeTime.contains(now) ? $0 : nil
+            }
+            return nil
+        }
+    }
+
+    private func validateGlucoseThresholds() -> GlucoseSchedulesValidationStatus? {
+        // This is on purpose
+        // we check all chedules for valid thresholds
+        for schedule in self.schedules {
+            if let low = schedule.lowAlarm, let high = schedule.highAlarm {
+                if low == high {
+                    return .error("One of your glucose schedules had the same value for low and high thresholds")
+                }
+                if low > high {
+                    return .error("One of your glucose schedules had a low threshold set above your high threshold")
+                }
+                //just for completness sake, this would never be called
+                if high < low {
+                    return .error("One of your glucose schedules had a high threshold set below your low threshold")
+                }
+            }
+        }
+        return nil
+    }
+
+    public func validateGlucoseSchedules() -> GlucoseSchedulesValidationStatus {
+        if let errors = validateGlucoseThresholds() {
+            return errors
+        }
+
+        // if we have zero or 1 enabled schedules, overlapping would not be possible
+        // (there is nothing to overlap on), so we skip interval check
+        guard self.enabledSchedules.count > 1 else {
+            return .success
+        }
+
+        var sameStartEnd = false
+        let intervals: [DateInterval] = enabledSchedules.compactMap({
+            var schedule = $0.getScheduleActiveToFrom()
+            if let start = schedule?.start, let end = schedule?.end {
+                if start == end {
+                    sameStartEnd = true
+                    return nil
+                }
+            }
+            // This compensates for Datetimes being closed range in nature
+            // example,
+            // interval1start = 12:00, interval1end=14:00
+            // interval2start = 14:00, interval2end=24:00
+            // interval1end and interval2 would collide when .intersect()-ing,
+            // so we change interval1end to 13:59:59
+            // and interval2end to 23:59:59
+            // This function is only used in the gui for validation, so this is acceptable
+            //
+            if let end = schedule?.end {
+                if let newEnd = Calendar.current.date(byAdding: .second, value: -1, to: end) {
+                    schedule?.end = newEnd
+                }
+            }
+            return schedule
+        })
+        if sameStartEnd {
+            return .error("One interval had the same start and end!")
+        }
+        if let intersects = intervals.intersect() {
+            print("Glucose schedule collided, not valid! \(intersects)")
+            return .error("Glucose schedules had overlapping time intervals")
+        }
+        return .success
+    }
+    //for convenience
+    public static var snoozedUntil: Date? {
+        UserDefaults.standard.snoozedUntil
+    }
+
+    public static func isSnoozed() -> Bool {
+        let now = Date()
+
+        if let snoozedUntil = snoozedUntil {
+            return snoozedUntil >= now
+        }
+        return false
+    }
+
+    public func getActiveAlarms(_ currentGlucoseInMGDL: Double) -> GlucoseScheduleAlarmResult {
+        for schedule in self.activeSchedules {
+            if let lowAlarm = schedule.lowAlarm, currentGlucoseInMGDL <= lowAlarm {
+                return .low
+            }
+            if let highAlarm = schedule.highAlarm, currentGlucoseInMGDL >= highAlarm {
+                return .high
+            }
+        }
+        return .none
+    }
+}
+
+class GlucoseSchedule: Codable, CustomStringConvertible {
+    var from: DateComponents?
+    var to: DateComponents?
+    var lowAlarm: Double?
+    var highAlarm: Double?
+    var enabled: Bool?
+
+    init() {
+    }
+
+    //glucose schedules are stored as standalone datecomponents (i.e. offsets)
+    //this takes the current start of day and adds those offsets,
+    // and returns a Dateinterval with those offsets applied
+    public func getScheduleActiveToFrom() -> DateInterval? {
+        guard let fromComponents = from, let toComponents = to else {
+            return nil
+        }
+
+        let now = Date()
+        let previousMidnight = Calendar.current.startOfDay(for: now)
+        let helper = Calendar.current.date(byAdding: .day, value: 1, to: previousMidnight)!
+        let nextMidnight = Calendar.current.startOfDay(for: helper)
+
+        let fromDate: Date? =  Calendar.current.date(byAdding: fromComponents, to: previousMidnight)
+        var toDate: Date?
+        if  toComponents.minute == 0 && toComponents.hour == 0 {
+            toDate = nextMidnight
+        } else {
+            toDate = Calendar.current.date(byAdding: toComponents, to: previousMidnight)!
+        }
+
+        if let fromDate = fromDate, let toDate = toDate, toDate >= fromDate {
+            return DateInterval(start: fromDate, end: toDate)
+        }
+        return nil
+    }
+    //stores the alarm. It does not synhronize the value with the underlaying userdefaults
+    //that is up to the caller of this class
+    public func storeLowAlarm(forUnit unit: HKUnit, lowAlarm: Double) {
+        if unit == HKUnit.millimolesPerLiter {
+            self.lowAlarm = lowAlarm * 18
+            return
+        }
+
+        self.lowAlarm = lowAlarm
+    }
+
+    public func retrieveLowAlarm(forUnit unit: HKUnit) -> Double? {
+        if let lowAlarm = self.lowAlarm {
+            if unit == HKUnit.millimolesPerLiter {
+                return (lowAlarm / 18).roundTo(places: 1)
+            } else {
+                return lowAlarm
+            }
+        }
+
+        return nil
+    }
+
+    //stores the alarm. It does not synhronize the value with the underlaying userdefaults
+    //that is up to the caller of this class
+    public func storeHighAlarm(forUnit unit: HKUnit, highAlarm: Double) {
+        if unit == HKUnit.millimolesPerLiter {
+            self.highAlarm = highAlarm * 18
+            return
+        }
+
+        self.highAlarm = highAlarm
+    }
+    public func retrieveHighAlarm(forUnit unit: HKUnit) -> Double? {
+        if let highAlarm = self.highAlarm {
+            if unit == HKUnit.millimolesPerLiter {
+                return (highAlarm / 18).roundTo(places: 1)
+            }
+            return highAlarm
+        }
+
+        return nil
+    }
+
+    var description: String {
+        "(from: \(String(describing: from)), to: \(String(describing: to)), low: \(String(describing: lowAlarm)), high: \(String(describing: highAlarm)), enabled: \(String(describing: enabled)))"
+    }
+}

+ 258 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/KeyChainManagerWrapper.swift

@@ -0,0 +1,258 @@
+//
+//  KeyChainWrapper.swift
+//  LibreTransmitter
+//
+//  Created by Bjørn Inge Berg on 11/07/2021.
+//  Copyright © 2021 Mark Wilson. All rights reserved.
+//
+
+import Foundation
+import Security
+
+
+public enum KeychainManagerError: Error {
+    case add(OSStatus)
+    case copy(OSStatus)
+    case delete(OSStatus)
+    case unknownResult
+}
+
+
+/**
+
+ Influenced by https://github.com/marketplacer/keychain-swift
+ */
+public struct KeychainManagerWrapper {
+    typealias Query = [String: NSObject]
+
+    public init() { }
+
+    var accessibility: CFString = kSecAttrAccessibleAfterFirstUnlock
+
+    var accessGroup: String?
+
+    static public var standard = KeychainManagerWrapper()
+
+    public struct InternetCredentials: Equatable {
+        public let username: String
+        public let password: String
+        public let url: URL
+
+        public init(username: String, password: String, url: URL) {
+            self.username = username
+            self.password = password
+            self.url = url
+        }
+    }
+
+    // MARK: - Convenience methods
+
+    private func query(by class: CFString) -> Query {
+        var query: Query = [kSecClass as String: `class`]
+
+        if let accessGroup = accessGroup {
+            query[kSecAttrAccessGroup as String] = accessGroup as NSObject?
+        }
+
+        return query
+    }
+
+
+
+
+    private func queryForInternetPassword(account: String? = nil, url: URL? = nil, label: String? = nil) -> Query {
+        var query = self.query(by: kSecClassInternetPassword)
+
+        if let account = account {
+            query[kSecAttrAccount as String] = account as NSObject?
+        }
+
+        if let url = url, let components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
+            for (key, value) in components.keychainAttributes {
+                query[key] = value
+            }
+        }
+
+        if let label = label {
+            query[kSecAttrLabel as String] = label as NSObject?
+        }
+
+        return query
+    }
+
+    private func updatedQuery(_ query: Query, withPassword password: Data) throws -> Query {
+        var query = query
+
+        query[kSecValueData as String] = password as NSObject?
+        query[kSecAttrAccessible as String] = accessibility
+
+        return query
+    }
+
+    private func updatedQuery(_ query: Query, withPassword password: String) throws -> Query {
+        guard let value = password.data(using: String.Encoding.utf8) else {
+            throw KeychainManagerError.add(errSecDecode)
+        }
+
+        return try updatedQuery(query, withPassword: value)
+    }
+
+    func delete(_ query: Query) throws {
+        let statusCode = SecItemDelete(query as CFDictionary)
+
+        guard statusCode == errSecSuccess || statusCode == errSecItemNotFound else {
+            throw KeychainManagerError.delete(statusCode)
+        }
+    }
+
+
+
+    // MARK – Internet Passwords
+
+    public func setInternetPassword(_ password: String, account: String, atURL url: URL, label: String? = nil) throws {
+        var query = try updatedQuery(queryForInternetPassword(account: account, url: url, label: label), withPassword: password)
+
+        query[kSecAttrAccount as String] = account as NSObject?
+
+        if let components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
+            for (key, value) in components.keychainAttributes {
+                query[key] = value
+            }
+        }
+
+        if let label = label {
+            query[kSecAttrLabel as String] = label as NSObject?
+        }
+
+        let statusCode = SecItemAdd(query as CFDictionary, nil)
+
+        guard statusCode == errSecSuccess else {
+            throw KeychainManagerError.add(statusCode)
+        }
+    }
+
+
+    public func replaceInternetCredentials(_ credentials: InternetCredentials?, forLabel label: String) throws {
+        let query = queryForInternetPassword(label: label)
+
+        try delete(query)
+
+        if let credentials = credentials {
+            try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url, label: label)
+        }
+    }
+
+    public func getInternetCredentials(account: String? = nil, url: URL? = nil, label: String? = nil) throws -> InternetCredentials {
+        var query = queryForInternetPassword(account: account, url: url, label: label)
+
+        query[kSecReturnData as String] = kCFBooleanTrue
+        query[kSecReturnAttributes as String] = kCFBooleanTrue
+        query[kSecMatchLimit as String] = kSecMatchLimitOne
+
+        var result: AnyObject?
+
+        let statusCode: OSStatus = SecItemCopyMatching(query as CFDictionary, &result)
+
+        guard statusCode == errSecSuccess else {
+            throw KeychainManagerError.copy(statusCode)
+        }
+
+        if  let result = result as? [AnyHashable: Any], let passwordData = result[kSecValueData as String] as? Data,
+            let password = String(data: passwordData, encoding: String.Encoding.utf8),
+            let url = URLComponents(keychainAttributes: result)?.url,
+            let username = result[kSecAttrAccount as String] as? String
+        {
+            return InternetCredentials(username: username, password: password, url: url)
+        }
+
+        throw KeychainManagerError.unknownResult
+    }
+}
+
+
+private enum SecurityProtocol {
+    case http
+    case https
+
+    init?(scheme: String?) {
+        switch scheme?.lowercased() {
+        case "http"?:
+            self = .http
+        case "https"?:
+            self = .https
+        default:
+            return nil
+        }
+    }
+
+    init?(secAttrProtocol: CFString) {
+        if secAttrProtocol == kSecAttrProtocolHTTP {
+            self = .http
+        } else if secAttrProtocol == kSecAttrProtocolHTTPS {
+            self = .https
+        } else {
+            return nil
+        }
+    }
+
+    var scheme: String {
+        switch self {
+        case .http:
+            return "http"
+        case .https:
+            return "https"
+        }
+    }
+
+    var secAttrProtocol: CFString {
+        switch self {
+        case .http:
+            return kSecAttrProtocolHTTP
+        case .https:
+            return kSecAttrProtocolHTTPS
+        }
+    }
+}
+
+
+private extension URLComponents {
+    init?(keychainAttributes: [AnyHashable: Any]) {
+        self.init()
+
+        if let secAttProtocol = keychainAttributes[kSecAttrProtocol as String] {
+            scheme = SecurityProtocol(secAttrProtocol: secAttProtocol as! CFString)?.scheme
+        }
+
+        host = keychainAttributes[kSecAttrServer as String] as? String
+
+        if let port = keychainAttributes[kSecAttrPort as String] as? Int, port > 0 {
+            self.port = port
+        }
+
+        if let path = keychainAttributes[kSecAttrPath as String] as? String {
+            self.path = path
+        }
+    }
+
+    var keychainAttributes: [String: NSObject] {
+        var query: [String: NSObject] = [:]
+
+        if let `protocol` = SecurityProtocol(scheme: scheme) {
+            query[kSecAttrProtocol as String] = `protocol`.secAttrProtocol
+        }
+
+        if let host = host {
+            query[kSecAttrServer as String] = host as NSObject
+        }
+
+        if let port = port {
+            query[kSecAttrPort as String] = port as NSObject
+        }
+
+        if !path.isEmpty {
+            query[kSecAttrPath as String] = path as NSObject
+        }
+
+        return query
+    }
+}

+ 60 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/LogExport.swift

@@ -0,0 +1,60 @@
+//
+//  LogExport.swift
+//  LibreTransmitter
+//
+//  Created by Bjørn Inge Berg on 22/09/2021.
+//  Copyright © 2021 Mark Wilson. All rights reserved.
+//
+
+import Foundation
+import OSLog
+
+
+@available(iOS 15, *)
+fileprivate func getLogEntries() throws -> [OSLogEntryLog] {
+    // Open the log store.
+    let logStore = try OSLogStore(scope: .currentProcessIdentifier)
+
+    // Get all the logs from the last hour.
+    let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600))
+
+    // Fetch log objects.
+    let allEntries = try logStore.getEntries(at: oneHourAgo)
+
+    // Filter the log to be relevant for our specific subsystem
+    // and remove other elements (signposts, etc).
+    return allEntries
+        .compactMap { $0 as? OSLogEntryLog }
+        //.filter { $0.subsystem == Features.logSubsystem }
+}
+
+@available(iOS 15, *)
+func getLogAsData() throws -> Data {
+    var data = Data()
+    let entries = try getLogEntries()
+    entries.forEach { log in
+        if let logstring = [log.date.ISO8601Format(), log.subsystem, log.category, "[["+log.composedMessage+"]]\r\n"]
+            .joined(separator: "||").data(using: .utf8, allowLossyConversion: false) {
+            data.append(logstring)
+        }
+
+    }
+    return data
+}
+
+func getLogs() throws -> Data {
+    var logs = Data()
+    if #available(iOS 15, *) {
+        logs = try getLogAsData()
+    }
+    return logs
+
+}
+
+
+public extension Logger {
+    init(forType atype: Any, forSubSystem subsystem: String=Features.logSubsystem) {
+        self.init(subsystem: subsystem, category: String(describing: atype)  )
+    }
+
+}

+ 18 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UIApplication+metadata.swift

@@ -0,0 +1,18 @@
+//
+//  UIApplication+metadata.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 30/12/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+private let prefix = "no-bjorninge-mm"
+enum AppMetaData {
+    static var allProperties: String {
+        Bundle.module.infoDictionary?.compactMap {
+            $0.key.starts(with: prefix) ? "\($0.key): \($0.value)" : nil
+        }.joined(separator: "\n") ?? "none"
+    }
+}

+ 184 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+Alarmsettings.swift

@@ -0,0 +1,184 @@
+//
+//  Userdefaults+Alarmsettings.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 20/04/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+extension UserDefaults {
+    private enum Key: String {
+        case glucoseSchedules = "no.bjorninge.glucoseschedules"
+
+        case mmAlwaysDisplayGlucose = "no.bjorninge.mmAlwaysDisplayGlucose"
+        case mmNotifyEveryXTimes = "no.bjorninge.mmNotifyEveryXTimes"
+        case mmGlucoseAlarmsVibrate = "no.bjorninge.mmGlucoseAlarmsVibrate"
+        case mmAlertLowBatteryWarning = "no.bjorninge.mmLowBatteryWarning"
+        case mmAlertInvalidSensorDetected = "no.bjorninge.mmInvalidSensorDetected"
+        //case mmAlertalarmNotifications
+        case mmAlertNewSensorDetected = "no.bjorninge.mmNewSensorDetected"
+        case mmAlertNoSensorDetected = "no.bjorninge.mmNoSensorDetected"
+        case mmGlucoseUnit = "no.bjorninge.mmGlucoseUnit"
+        case mmAlertSensorSoonExpire = "no.bjorninge.mmAlertSensorSoonExpire"
+        case mmSnoozedUntil = "no.bjorninge.mmSnoozedUntil"
+        case mmShowTransmitterBattery = "no.bjorninge.mmShowTransmitterBattery"
+    }
+    /*
+     case always
+     case lowBattery
+     case invalidSensorDetected
+     //case alarmNotifications
+     case newSensorDetected
+     case noSensorDetected
+     case unit
+     */
+    public func optionalBool(forKey defaultName: String) -> Bool? {
+        if let value = value(forKey: defaultName) {
+            return value as? Bool
+        }
+        return nil
+    }
+
+    var mmAlwaysDisplayGlucose: Bool {
+        get {
+            optionalBool(forKey: Key.mmAlwaysDisplayGlucose.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmAlwaysDisplayGlucose.rawValue)
+        }
+    }
+    var mmNotifyEveryXTimes: Int {
+        get {
+            integer(forKey: Key.mmNotifyEveryXTimes.rawValue)
+        }
+        set {
+            set(newValue, forKey: Key.mmNotifyEveryXTimes.rawValue)
+        }
+    }
+
+    var mmAlertLowBatteryWarning: Bool {
+        get {
+            optionalBool(forKey: Key.mmAlertLowBatteryWarning.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmAlertLowBatteryWarning.rawValue)
+        }
+    }
+    var mmAlertInvalidSensorDetected: Bool {
+        get {
+            optionalBool(forKey: Key.mmAlertInvalidSensorDetected.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmAlertInvalidSensorDetected.rawValue)
+        }
+    }
+
+    var mmAlertNewSensorDetected: Bool {
+        get {
+            optionalBool(forKey: Key.mmAlertNewSensorDetected.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmAlertNewSensorDetected.rawValue)
+        }
+    }
+
+    var mmAlertNoSensorDetected: Bool {
+        get {
+            optionalBool(forKey: Key.mmAlertNoSensorDetected.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmAlertNoSensorDetected.rawValue)
+        }
+    }
+
+    var mmAlertWillSoonExpire: Bool {
+        get {
+            optionalBool(forKey: Key.mmAlertSensorSoonExpire.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmAlertSensorSoonExpire.rawValue)
+        }
+    }
+
+    var mmGlucoseAlarmsVibrate: Bool {
+        get {
+            optionalBool(forKey: Key.mmGlucoseAlarmsVibrate.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmGlucoseAlarmsVibrate.rawValue)
+        }
+    }
+
+    var mmShowTransmitterBattery: Bool {
+        get {
+            optionalBool(forKey: Key.mmShowTransmitterBattery.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmShowTransmitterBattery.rawValue)
+        }
+    }
+
+    var allNotificationToggles: [Bool] {
+        [mmAlwaysDisplayGlucose, mmAlertLowBatteryWarning, mmAlertInvalidSensorDetected, mmAlertNewSensorDetected, mmAlertNoSensorDetected, mmAlertWillSoonExpire, mmGlucoseAlarmsVibrate, mmShowTransmitterBattery]
+    }
+
+    //intentionally only supports mgdl and mmol
+    var mmGlucoseUnit: HKUnit? {
+        get {
+            if let textUnit = string(forKey: Key.mmGlucoseUnit.rawValue) {
+                if textUnit == "mmol" {
+                    return HKUnit.millimolesPerLiter
+                } else if textUnit == "mgdl" {
+                    return HKUnit.milligramsPerDeciliter
+                }
+            }
+
+            return nil
+        }
+        set {
+            if newValue == HKUnit.milligramsPerDeciliter {
+                set("mgdl", forKey: Key.mmGlucoseUnit.rawValue)
+            } else if newValue == HKUnit.millimolesPerLiter {
+                set("mmol", forKey: Key.mmGlucoseUnit.rawValue)
+            }
+        }
+    }
+
+    var enabledSchedules: [GlucoseSchedule]? {
+        glucoseSchedules?.schedules.compactMap({ schedule -> GlucoseSchedule? in
+            if schedule.enabled ?? false {
+                return schedule
+            }
+            return nil
+        })
+    }
+    var snoozedUntil: Date? {
+        get {
+            object(forKey: Key.mmSnoozedUntil.rawValue) as? Date
+        }
+        set {
+            set(newValue, forKey: Key.mmSnoozedUntil.rawValue)
+        }
+    }
+    var glucoseSchedules: GlucoseScheduleList? {
+        get {
+            if let savedGlucoseSchedules = object(forKey: Key.glucoseSchedules.rawValue) as? Data {
+                let decoder = JSONDecoder()
+                if let loadedGlucoseSchedules = try? decoder.decode(GlucoseScheduleList.self, from: savedGlucoseSchedules) {
+                    return loadedGlucoseSchedules
+                }
+            }
+
+            return GlucoseScheduleList()
+        }
+        set {
+            let encoder = JSONEncoder()
+            if let val = newValue, let encoded = try? encoder.encode(val) {
+                set(encoded, forKey: Key.glucoseSchedules.rawValue)
+            }
+        }
+    }
+}

+ 48 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+Bluetooth.swift

@@ -0,0 +1,48 @@
+//
+//  UserDefaults+Bluetooth.swift
+//  MiaomiaoClientUI
+//
+//  Created by Bjørn Inge Berg on 27/07/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+//import MiaomiaoClient
+
+extension UserDefaults {
+    private enum Key: String {
+        case bluetoothDeviceUUIDString = "no.bjorninge.bluetoothDeviceUUIDString"
+        case libre2UiD = "no.bjorninge.libre2uid"
+    }
+
+    public var preSelectedUid: Data? {
+        get {
+            return data(forKey: Key.libre2UiD.rawValue)
+
+        }
+        set {
+            if let newValue = newValue {
+                set(newValue, forKey: Key.libre2UiD.rawValue)
+            } else {
+                print("Removing preSelectedUid")
+                removeObject(forKey: Key.libre2UiD.rawValue)
+            }
+        }
+    }
+
+    public var preSelectedDevice: String? {
+        get {
+            if let astr = string(forKey: Key.bluetoothDeviceUUIDString.rawValue) {
+                return astr.count > 0 ? astr : nil
+            }
+            return nil
+        }
+        set {
+            if let newValue = newValue {
+                set(newValue, forKey: Key.bluetoothDeviceUUIDString.rawValue)
+            } else {
+                removeObject(forKey: Key.bluetoothDeviceUUIDString.rawValue)
+            }
+        }
+    }
+}

+ 35 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+GlucoseSettings.swift

@@ -0,0 +1,35 @@
+//
+//  Userdefaults+Alarmsettings.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 20/04/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+extension UserDefaults {
+    private enum Key: String {
+        case mmBackfillFromHistory = "no.bjorninge.mmBackfillFromHistory"
+        case mmBackfillFromTrend = "no.bjorninge.mmBackfillFromTrend"
+    }
+
+    var mmBackfillFromHistory: Bool {
+        get {
+             optionalBool(forKey: Key.mmBackfillFromHistory.rawValue) ?? true
+        }
+        set {
+            set(newValue, forKey: Key.mmBackfillFromHistory.rawValue)
+        }
+    }
+
+    var mmBackfillFromTrend: Bool {
+        get {
+            optionalBool(forKey: Key.mmBackfillFromTrend.rawValue) ?? false
+        }
+        set {
+            set(newValue, forKey: Key.mmBackfillFromTrend.rawValue)
+        }
+    }
+}

+ 38 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/UnfairLock.swift

@@ -0,0 +1,38 @@
+//
+//  UnfairLock.swift
+//  LoopKit Example
+//
+//  Created by Pete Schwamb on 3/22/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+// Source: http://www.russbishop.net/the-law
+
+import Foundation
+
+public class UnfairLock {
+    private var _lock: UnsafeMutablePointer<os_unfair_lock>
+
+    public init() {
+        _lock = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
+        _lock.initialize(to: os_unfair_lock())
+    }
+
+    deinit {
+        _lock.deallocate()
+    }
+
+    public func withLock<ReturnValue>(_ f: () throws -> ReturnValue) rethrows -> ReturnValue {
+        os_unfair_lock_lock(_lock)
+        defer { os_unfair_lock_unlock(_lock) }
+        return try f()
+    }
+
+    public func assertOwned() {
+        os_unfair_lock_assert_owner(_lock)
+    }
+
+    public func assertNotOwned() {
+        os_unfair_lock_assert_not_owner(_lock)
+    }
+}

+ 83 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/WeakSynchronizedDelegate.swift

@@ -0,0 +1,83 @@
+//
+//  WeakSynchronizedDelegate.swift
+//  LoopKit
+//
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public class WeakSynchronizedDelegate<Delegate> {
+
+    private let lock = UnfairLock()
+    private weak var _delegate: AnyObject?
+    private var _queue: DispatchQueue
+
+    public init(queue: DispatchQueue = .main) {
+        _queue = queue
+    }
+
+    public var delegate: Delegate? {
+        get {
+            return lock.withLock {
+                return _delegate as? Delegate
+            }
+        }
+        set {
+            lock.withLock {
+                _delegate = newValue as AnyObject
+            }
+        }
+    }
+
+    public var queue: DispatchQueue! {
+        get {
+            return lock.withLock {
+                return _queue
+            }
+        }
+        set {
+            lock.withLock {
+                _queue = newValue ?? .main
+            }
+        }
+    }
+    
+    public func notify(_ block: @escaping (_ delegate: Delegate?) -> Void) {
+        var delegate: Delegate?
+        var queue: DispatchQueue!
+
+        lock.withLock {
+            delegate = _delegate as? Delegate
+            queue = _queue
+        }
+
+        queue.async {
+            block(delegate)
+        }
+    }
+    
+    public func notifyDelayed(by interval: TimeInterval, _ block: @escaping (_ delegate: Delegate?) -> Void) {
+        var delegate: Delegate?
+        var queue: DispatchQueue!
+
+        lock.withLock {
+            delegate = _delegate as? Delegate
+            queue = _queue
+        }
+
+        queue.asyncAfter(deadline: .now() + interval) {
+            block(delegate)
+        }
+    }
+
+    public func call<ReturnType>(_ block: (_ delegate: Delegate?) -> ReturnType) -> ReturnType {
+        return lock.withLock { () -> ReturnType in
+            var result: ReturnType!
+            _queue.sync {
+                result = block(_delegate as? Delegate)
+            }
+            return result
+        }
+    }
+}

+ 18 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/CompletionNotifying.swift

@@ -0,0 +1,18 @@
+//
+//  CompletionNotifying.swift
+//  LoopKitUI
+//
+//  Created by Pete Schwamb on 1/29/19.
+//  Copyright © 2019 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public protocol CompletionDelegate: AnyObject {
+    func completionNotifyingDidComplete(_ object: CompletionNotifying)
+}
+
+public protocol CompletionNotifying {
+    var completionDelegate: CompletionDelegate? { set get }
+}
+

+ 126 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/ConcreteGlucoseDisplayable.swift

@@ -0,0 +1,126 @@
+//
+//  ConcreteSensorDisplayable.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 04/11/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+public struct ConcreteGlucoseDisplayable: GlucoseDisplayable {
+    public var glucoseRangeCategory: GlucoseRangeCategory?
+
+    public var isStateValid: Bool
+
+    public var trendType: GlucoseTrend?
+
+    public var isLocal: Bool
+
+    public var batteries : [(name: String, percentage: Int)]?
+}
+
+public enum GlucoseRangeCategory: Int, CaseIterable {
+    case belowRange
+    case urgentLow
+    case low
+    case normal
+    case high
+    case aboveRange
+}
+
+public enum GlucoseTrend: Int, CaseIterable {
+    case upUpUp       = 1
+    case upUp         = 2
+    case up           = 3
+    case flat         = 4
+    case down         = 5
+    case downDown     = 6
+    case downDownDown = 7
+
+    public var symbol: String {
+        switch self {
+        case .upUpUp:
+            return "⇈"
+        case .upUp:
+            return "↑"
+        case .up:
+            return "↗︎"
+        case .flat:
+            return "→"
+        case .down:
+            return "↘︎"
+        case .downDown:
+            return "↓"
+        case .downDownDown:
+            return "⇊"
+        }
+    }
+
+    public var arrows: String {
+        switch self {
+        case .upUpUp:
+            return "↑↑"
+        case .upUp:
+            return "↑"
+        case .up:
+            return "↗︎"
+        case .flat:
+            return "→"
+        case .down:
+            return "↘︎"
+        case .downDown:
+            return "↓"
+        case .downDownDown:
+            return "↓↓"
+        }
+    }
+
+    public var localizedDescription: String {
+        switch self {
+        case .upUpUp:
+            return LocalizedString("Rising very fast", comment: "Glucose trend up-up-up")
+        case .upUp:
+            return LocalizedString("Rising fast", comment: "Glucose trend up-up")
+        case .up:
+            return LocalizedString("Rising", comment: "Glucose trend up")
+        case .flat:
+            return LocalizedString("Flat", comment: "Glucose trend flat")
+        case .down:
+            return LocalizedString("Falling", comment: "Glucose trend down")
+        case .downDown:
+            return LocalizedString("Falling fast", comment: "Glucose trend down-down")
+        case .downDownDown:
+            return LocalizedString("Falling very fast", comment: "Glucose trend down-down-down")
+        }
+    }
+}
+
+public protocol GlucoseDisplayable {
+    /// Returns whether the current state is valid
+    var isStateValid: Bool { get }
+
+    /// Describes the state of the sensor in the current localization
+    var stateDescription: String { get }
+
+    /// Enumerates the trend of the sensor values
+    var trendType: GlucoseTrend? { get }
+
+    /// Returns whether the data is from a locally-connected device
+    var isLocal: Bool { get }
+
+    /// enumerates the glucose value type (e.g., normal, low, high)
+    var glucoseRangeCategory: GlucoseRangeCategory? { get }
+}
+
+
+extension GlucoseDisplayable {
+    public var stateDescription: String {
+        if isStateValid {
+            return LocalizedString("OK", comment: "Sensor state description for the valid state")
+        } else {
+            return LocalizedString("Needs Attention", comment: "Sensor state description for the non-valid state")
+        }
+    }
+}

+ 24 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/DeviceLogEntryType.swift

@@ -0,0 +1,24 @@
+//
+//  DeviceLogEntryType.swift
+//  LoopKit
+//
+//  Created by Pete Schwamb on 1/13/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public enum DeviceLogEntryType: String {
+    /// Log entry related to data or commands sent to the device.
+    case send
+    /// Log entry related to data or events received from the device.
+    case receive
+    /// Log entry related to any errors from the device's SDK or the device itself.
+    case error
+    /// Log entry related to a delegate call from the device's SDK (for example, acknowledgement of receiving a command).
+    case delegate
+    /// Log entry related to a response from a delegate call (for example, acknowledgement of executing a command).
+    case delegateResponse
+    /// Log entry related to any device connection activities (e.g. scanning, connecting, disconnecting, reconnecting, etc.).
+    case connection
+}

+ 43 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Features.swift

@@ -0,0 +1,43 @@
+//
+//  Features.swift
+//  LibreTransmitter
+//
+//  Created by Bjørn Inge Berg on 30/08/2021.
+//  Copyright © 2021 Mark Wilson. All rights reserved.
+//
+
+import Foundation
+
+#if canImport(CoreNFC)
+import CoreNFC
+#endif
+
+public final class Features{
+
+    static public var logSubsystem = "no.bjorninge.libre"
+
+    static var phoneNFCAvailable: Bool {
+        #if canImport(CoreNFC)
+        if NSClassFromString("NFCNDEFReaderSession") == nil {
+            return false
+            
+        }
+
+        return NFCNDEFReaderSession.readingAvailable
+        #else
+        return false
+        #endif
+    }
+
+    static var supportsLogExport: Bool {
+        if #available(iOS 15, *) {
+            return true
+        }
+        return false
+    }
+
+
+
+
+}
+

+ 225 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreGlucose.swift

@@ -0,0 +1,225 @@
+//
+//  MiaomiaoClient.h
+//  MiaomiaoClient
+//
+
+import Foundation
+import HealthKit
+import os.log
+
+
+fileprivate var logger = Logger(forType: "LibreGlucose")
+
+
+public struct LibreGlucose {
+    public let unsmoothedGlucose: Double
+    public var glucoseDouble: Double
+    public var error = [MeasurementError.OK]
+    public var glucose: UInt16 {
+        UInt16(glucoseDouble.rounded())
+    }
+    //trend is deprecated here, it should only be calculated once in latestbackfill
+    //public var trend: UInt8
+    public var timestamp: Date
+    //public let collector: String?
+
+    public static func timeDifference(oldGlucose: LibreGlucose, newGlucose: LibreGlucose) -> TimeInterval {
+        newGlucose.startDate.timeIntervalSince(oldGlucose.startDate)
+    }
+
+    public var syncId: String {
+        "\(Int(self.startDate.timeIntervalSince1970))\(self.unsmoothedGlucose)"
+    }
+
+    public var isStateValid: Bool {
+        // We know that the official libre algorithm doesn't produce values
+        // below 39. However, both the raw sensor contents and the derived algorithm
+        // supports values down to 0 without issues. A bit uncertain if nightscout and loop will work with values below 1, so we restrict this to 1
+        glucose >= 1
+    }
+
+    public func GetGlucoseTrend(last: Self) -> GlucoseTrend {
+        Self.GetGlucoseTrend(current: self, last: last)
+    }
+}
+
+extension LibreGlucose: GlucoseValue {
+    public var startDate: Date {
+        timestamp
+    }
+
+    public var quantity: HKQuantity {
+        .init(unit: .milligramsPerDeciliter, doubleValue: glucoseDouble)
+    }
+}
+
+extension LibreGlucose {
+    public var description: String {
+        guard let glucoseUnit = UserDefaults.standard.mmGlucoseUnit, let formatter = LibreGlucose.dynamicFormatter, let formatted = formatter.string(from: self.quantity, for: glucoseUnit) else {
+            logger.debug("dabear:: glucose unit was not recognized, aborting")
+            return "Unknown"
+        }
+
+        return formatted
+    }
+    private static var glucoseFormatterMgdl: QuantityFormatter = {
+        let formatter = QuantityFormatter()
+        formatter.setPreferredNumberFormatter(for: HKUnit.milligramsPerDeciliter)
+        return formatter
+    }()
+
+    private static var glucoseFormatterMmol: QuantityFormatter = {
+        let formatter = QuantityFormatter()
+        formatter.setPreferredNumberFormatter(for: HKUnit.millimolesPerLiter)
+        return formatter
+    }()
+
+    public static var dynamicFormatter: QuantityFormatter? {
+        guard let glucoseUnit = UserDefaults.standard.mmGlucoseUnit else {
+            logger.debug("dabear:: glucose unit was not recognized, aborting")
+            return nil
+        }
+
+        return (glucoseUnit == HKUnit.milligramsPerDeciliter ? glucoseFormatterMgdl : glucoseFormatterMmol)
+    }
+
+    public static func glucoseDiffDesc(oldValue: Self, newValue: Self) -> String {
+        guard let glucoseUnit = UserDefaults.standard.mmGlucoseUnit else {
+            logger.debug("dabear:: glucose unit was not recognized, aborting")
+            return "Unknown"
+        }
+
+        var stringValue = [String]()
+
+        var diff = newValue.glucoseDouble - oldValue.glucoseDouble
+        let sign = diff < 0 ? "-" : "+"
+
+        if diff == 0 {
+            stringValue.append( "\(sign) 0")
+        } else {
+            diff = abs(diff)
+            let asObj = LibreGlucose(
+                unsmoothedGlucose: diff,
+                glucoseDouble: diff,
+                timestamp: Date())
+            if let formatted = dynamicFormatter?.string(from: asObj.quantity, for: glucoseUnit) {
+                stringValue.append( "\(sign) \(formatted)")
+            }
+        }
+
+        return stringValue.joined(separator: ",")
+    }
+}
+
+extension LibreGlucose {
+    static func calculateSlope(current: Self, last: Self) -> Double {
+        if current.timestamp == last.timestamp {
+            return 0.0
+        }
+
+        let _curr = Double(current.timestamp.timeIntervalSince1970 * 1_000)
+        let _last = Double(last.timestamp.timeIntervalSince1970 * 1_000)
+
+        return (Double(last.unsmoothedGlucose) - Double(current.unsmoothedGlucose)) / (_last - _curr)
+    }
+
+    static func calculateSlopeByMinute(current: Self, last: Self) -> Double {
+        return calculateSlope(current: current, last: last) * 60_000
+    }
+
+    static func GetGlucoseTrend(current: Self?, last: Self?) -> GlucoseTrend {
+
+        guard let current = current, let last = last else {
+            return  .flat
+        }
+
+        let  s = calculateSlopeByMinute(current: current, last: last)
+
+
+        switch s {
+        case _ where s <= (-3.5):
+            return .downDownDown
+        case _ where s <= (-2):
+            return .downDown
+        case _ where s <= (-1):
+            return .down
+        case _ where s <= (1):
+            return .flat
+        case _ where s <= (2):
+            return .up
+        case _ where s <= (3.5):
+            return .upUp
+        case _ where s <= (40):
+            return .flat //flat is the new (tm) "unknown"!
+
+        default:
+
+            return .flat
+        }
+    }
+}
+
+extension LibreGlucose {
+    static func fromHistoryMeasurements(_ measurements: [Measurement], nativeCalibrationData: SensorData.CalibrationInfo) -> [LibreGlucose] {
+        var arr = [LibreGlucose]()
+
+        for historical in measurements {
+            let glucose = LibreGlucose(
+                //unsmoothedGlucose: historical.temperatureAlgorithmGlucose,
+                //glucoseDouble: historical.temperatureAlgorithmGlucose,
+                unsmoothedGlucose: historical.roundedGlucoseValueFromRaw2(calibrationInfo: nativeCalibrationData),
+                glucoseDouble: historical.roundedGlucoseValueFromRaw2(calibrationInfo: nativeCalibrationData),
+                error: historical.error,
+                timestamp: historical.date)
+
+            if glucose.glucoseDouble > 0 {
+                arr.append(glucose)
+            }
+        }
+
+        return arr
+    }
+
+    static func fromTrendMeasurements(_ measurements: [Measurement], nativeCalibrationData: SensorData.CalibrationInfo, returnAll: Bool) -> [LibreGlucose] {
+        var arr = [LibreGlucose]()
+
+        var shouldSmoothGlucose = true
+        for trend in measurements {
+            // trend arrows on each libreglucose value is not needed
+            // instead we calculate it once when latestbackfill is set, which in turn sets
+            // the sensordisplayable property
+            let glucose = LibreGlucose(
+                //unsmoothedGlucose: trend.temperatureAlgorithmGlucose,
+                unsmoothedGlucose: trend.roundedGlucoseValueFromRaw2(calibrationInfo: nativeCalibrationData),
+                glucoseDouble: 0.0,
+                error: trend.error,
+                timestamp: trend.date)
+            // if sensor is ripped off body while transmitter is attached, values below 1 might be created
+
+            if glucose.unsmoothedGlucose > 0 {
+                arr.append(glucose)
+            }
+
+            // Just for expliciticity, if one of the values are 0,
+            // then the rest of the values should not be smoothed
+            if glucose.unsmoothedGlucose <= 0 {
+                shouldSmoothGlucose = false
+            }
+        }
+
+
+        if shouldSmoothGlucose {
+            arr = CalculateSmothedData5Points(origtrends: arr)
+        } else {
+            for i in 0 ..< arr.count {
+                arr[i].glucoseDouble = arr[i].unsmoothedGlucose
+            }
+        }
+
+        if !returnAll, let first = arr.first {
+            return [first]
+        }
+
+        return arr
+    }
+}

+ 75 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/Calibration.swift

@@ -0,0 +1,75 @@
+//
+//  Calibration.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 05/03/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+private let LibreCalibrationLabel =  "https://LibreCalibrationLabelNative.doesnot.exist.com"
+private let LibreCalibrationUrl = URL(string: LibreCalibrationLabel)!
+private let LibreUsername = "LibreUsername"
+
+fileprivate var logger = Logger(forType: "KeychainManagerCalibration")
+
+public extension KeychainManagerWrapper {
+    func setLibreNativeCalibrationData(_ calibrationData: SensorData.CalibrationInfo?) throws {
+        var credentials: InternetCredentials? = nil
+        if let calibrationData = calibrationData {
+            credentials = InternetCredentials(username: LibreUsername, password: serializeNativeAlgorithmParameters(calibrationData), url: LibreCalibrationUrl)
+        }
+        logger.debug("dabear: Setting calibrationdata to \(String(describing: calibrationData))")
+        try replaceInternetCredentials(credentials, forLabel: LibreCalibrationLabel)
+    }
+
+    func getLibreNativeCalibrationData() -> SensorData.CalibrationInfo? {
+        do { // Silence all errors and return nil
+            let credentials = try getInternetCredentials(label: LibreCalibrationLabel)
+            return deserializeNativeAlgorithmParameters(text: credentials.password)
+        } catch {
+
+            return nil
+        }
+    }
+}
+
+public func calibrateSensor(sensordata: SensorData, callback: @escaping (SensorData.CalibrationInfo) -> Void) {
+    
+    let params = sensordata.calibrationData
+    callback(params)
+}
+
+private func serializeNativeAlgorithmParameters(_ params: SensorData.CalibrationInfo) -> String {
+    let encoder = JSONEncoder()
+    encoder.outputFormatting = .prettyPrinted
+
+    var aString = ""
+    do {
+        let jsonData = try encoder.encode(params)
+
+        if let jsonString = String(data: jsonData, encoding: .utf8) {
+            aString = jsonString
+        }
+    } catch {
+        logger.debug("Could not serialize parameters: \(error.localizedDescription)")
+    }
+    return aString
+}
+
+private func deserializeNativeAlgorithmParameters(text: String) -> SensorData.CalibrationInfo? {
+    if let jsonData = text.data(using: .utf8) {
+        let decoder = JSONDecoder()
+
+        do {
+            return try decoder.decode(SensorData.CalibrationInfo.self, from: jsonData)
+        } catch {
+            logger.debug("Could not create instance: \(error.localizedDescription)")
+        }
+    } else {
+        logger.debug("Did not create instance")
+    }
+    return nil
+}

+ 32 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/GlucoseAlgorithm/GlucoseFromRaw.swift

@@ -0,0 +1,32 @@
+//
+//  GlucoseFromRaw.swift
+//
+
+import Foundation
+import RawGlucose
+
+extension MeasurementProtocol {
+    func roundedGlucoseValueFromRaw(calibrationInfo: SensorData.CalibrationInfo) -> Int {
+        Int(round(glucoseValueFromRaw(calibrationInfo: calibrationInfo)))
+    }
+
+    func roundedGlucoseValueFromRaw2(calibrationInfo: SensorData.CalibrationInfo) -> Double{
+        round(glucoseValueFromRaw(calibrationInfo: calibrationInfo))
+    }
+
+    
+    func glucoseValueFromRaw(calibrationInfo: SensorData.CalibrationInfo) -> Double {
+        RawGlucose.glucoseValueFromRaw(
+            rawTemperature: Double(self.rawTemperature),
+            rawTemperatureAdjustment: Double(self.rawTemperatureAdjustment),
+            rawGlucose: Double(self.rawGlucose),
+            i1: calibrationInfo.i1,
+            i2: calibrationInfo.i2,
+            i3: calibrationInfo.i3,
+            i4: calibrationInfo.i4,
+            i5: calibrationInfo.i5,
+            i6: calibrationInfo.i6
+        )
+    }
+}
+

+ 40 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/GlucoseAlgorithm/GlucoseSmoothing.swift

@@ -0,0 +1,40 @@
+//
+//  GlucoseSmoothing.swift
+//  MiaomiaoClientUI
+//
+//  Created by Bjørn Inge Berg on 25/03/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+//https://github.com/NightscoutFoundation/xDrip/pull/828/files
+
+//private func trendToLibreGlucose(_ measurements: [Measurement]) -> [LibreGlucose]?{
+
+func CalculateSmothedData5Points(origtrends: [LibreGlucose]) -> [LibreGlucose] {
+    // In all places in the code, there should be exactly 16 points.
+    // Since that might change, and I'm doing an average of 5, then in the case of less then 5 points,
+    // I'll only copy the data as is (to make sure there are reasonable values when the function returns).
+
+    var trends = origtrends
+    //this is an adoptation, doesn't follow the original directly
+    if trends.count < 5 {
+        for i in 0 ..< trends.count {
+            trends[i].glucoseDouble = trends[i].unsmoothedGlucose
+        }
+
+        return trends
+    }
+    for i in 0 ..< trends.count - 4 {
+        trends[i].glucoseDouble = (trends[i].unsmoothedGlucose + trends[i + 1].unsmoothedGlucose + trends[i + 2].unsmoothedGlucose + trends[i + 3].unsmoothedGlucose + trends[i + 4].unsmoothedGlucose) / 5
+    }
+    trends[trends.count - 4].glucoseDouble = (trends[trends.count - 4].unsmoothedGlucose + trends[trends.count - 3].unsmoothedGlucose + trends[trends.count - 2].unsmoothedGlucose + trends[trends.count - 1].unsmoothedGlucose) / 4
+    trends[trends.count - 3].glucoseDouble = (trends[trends.count - 3].unsmoothedGlucose + trends[trends.count - 2].unsmoothedGlucose + trends[trends.count - 1].unsmoothedGlucose ) / 3
+
+    trends[trends.count - 2].glucoseDouble = (trends[trends.count - 2].unsmoothedGlucose + trends[trends.count - 1].unsmoothedGlucose ) / 2
+
+    trends[trends.count - 1].glucoseDouble = trends[trends.count - 2].glucoseDouble
+
+    return trends
+}

+ 42 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/LibreError.swift

@@ -0,0 +1,42 @@
+//
+//  LibreError.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 05/03/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+
+import Foundation
+
+public enum LibreError: Error {
+    case noSensorData
+    case noCalibrationData
+    case invalidCalibrationData
+    case checksumValidationError
+    case expiredSensor
+    case invalidAutoCalibrationCredentials
+    case encryptedSensor
+    case noValidSensorData
+
+    public var errorDescription: String {
+        switch self {
+        case .noSensorData:
+            return "No sensor data present"
+        case .noValidSensorData:
+            return "No valid sensor data present, but sensor is running. Maybe due to sensor being off-body?"
+
+        case .noCalibrationData:
+            return "No calibration data present"
+        case .invalidCalibrationData:
+            return "invalid calibration data detected"
+        case .checksumValidationError:
+            return "Checksum Validation Error "
+        case .expiredSensor:
+            return "Sensor has expired"
+        case .invalidAutoCalibrationCredentials:
+            return "Invalid Auto Calibration Credentials"
+        case .encryptedSensor:
+            return "Encrypted or unsupported libre sensor detected."
+        }
+    }
+}

+ 63 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/LimitedQueue.swift

@@ -0,0 +1,63 @@
+//
+//  LimitedQueue.swift
+//  MiaomiaoClient
+//
+//  Created by Bjørn Inge Berg on 03/03/2020.
+//  Copyright © 2020 Bjørn Inge Vikhammermo Berg. All rights reserved.
+//
+
+import Foundation
+
+public struct LimitedQueue<T: Codable>: Codable {
+  public var array  = [T]()
+  var limit: Int = 10
+
+  mutating func enqueue(_ element: T) {
+    while array.count >= limit {
+        array.removeFirst()
+    }
+    array.append(element)
+  }
+
+  mutating func dequeue() -> T? {
+    array.isEmpty ? nil : array.removeFirst()
+  }
+}
+extension UserDefaults {
+    private enum Key: String {
+        case queuedSensorData = "no.bjorninge.queuedSensorData"
+        case shouldPersistSensorData = "no.bjorninge.shouldPersistSensorData"
+    }
+
+    var shouldPersistSensorData: Bool {
+        get {
+            optionalBool(forKey: Key.shouldPersistSensorData.rawValue) ?? false
+        }
+        set {
+            set(newValue, forKey: Key.shouldPersistSensorData.rawValue)
+        }
+    }
+
+    public var queuedSensorData: LimitedQueue<SensorData>? {
+        get {
+            guard let data = object(forKey: Key.queuedSensorData.rawValue) as? Data else {
+                return nil
+            }
+
+            let decoder = JSONDecoder()
+            guard let q = try? decoder.decode(LimitedQueue<SensorData>.self, from: data) else {
+                return nil
+            }
+
+            return q
+        }
+        set {
+            let encoder = JSONEncoder()
+            if let val = newValue, let encoded = try? encoder.encode(val) {
+                set(encoded, forKey: Key.queuedSensorData.rawValue)
+            } else {
+                removeObject(forKey: Key.queuedSensorData.rawValue)
+            }
+        }
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 105 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/CRC.swift


+ 142 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/Measurement.swift

@@ -0,0 +1,142 @@
+//
+//  Measurement.swift
+//  LibreMonitor
+//
+//  Created by Uwe Petersen on 25.08.16.
+//  Copyright © 2016 Uwe Petersen. All rights reserved.
+//
+
+import Foundation
+
+protocol MeasurementProtocol {
+    var rawGlucose: Int { get }
+    /// The raw temperature as read from the sensor
+    var rawTemperature: Int { get }
+
+    var rawTemperatureAdjustment: Int { get }
+
+    var error : [MeasurementError] { get}
+}
+
+public enum MeasurementError: Int, CaseIterable {
+    case OK = 0
+    case SD14_FIFO_OVERFLOW      = 1
+    case FILTER_DELTA            = 0x02
+    case WORK_VOLTAGE            = 0x04
+    case PEAK_DELTA_EXCEEDED     = 0x08
+    case AVG_DELTA_EXCEEDED      = 0x10
+    case RF                      = 0x20
+    case REF_R                   = 0x40
+    case SIGNAL_SATURATED        = 128    //   0x80
+    case SENSOR_SIGNAL_LOW       = 256    //  0x100
+    case THERMISTOR_OUT_OF_RANGE = 2048   //  0x800
+
+    case TEMP_HIGH               = 8192   // 0x2000
+    case TEMP_LOW                = 16384  // 0x4000
+    case INVALID_DATA            = 32768  // 0x8000
+
+    static var allErrorCodes : [MeasurementError] {
+        var allErrorCases = MeasurementError.allCases
+        allErrorCases.removeAll { $0 == .OK}
+        return allErrorCases
+    }
+}
+
+
+
+
+
+
+struct SimplifiedMeasurement: MeasurementProtocol {
+    var rawGlucose: Int
+
+    var rawTemperature: Int
+
+    var rawTemperatureAdjustment: Int = 0
+
+    var error = [MeasurementError.OK]
+}
+
+/// Structure for one glucose measurement including value, date and raw data bytes
+public struct Measurement: MeasurementProtocol {
+    /// The date for this measurement
+    let date: Date
+    /// The minute counter for this measurement
+    let counter: Int
+    /// The bytes as read from the sensor. All data is derived from this \"raw data"
+    let bytes: [UInt8]
+    /// The bytes as String
+    let byteString: String
+    /// The raw glucose as read from the sensor
+    let rawGlucose: Int
+    /// The raw temperature as read from the sensor
+    let rawTemperature: Int
+
+    let rawTemperatureAdjustment: Int
+
+    let error : [MeasurementError]
+
+    let idValue : Int
+
+    init(date: Date, rawGlucose: Int, rawTemperature: Int, rawTemperatureAdjustment: Int, idValue: Int = 0) {
+        self.date = date
+        self.rawGlucose = rawGlucose
+        self.rawTemperature = rawTemperature
+        self.rawTemperatureAdjustment = rawTemperatureAdjustment
+
+        //not really needed when setting the other properties above explicitly
+        self.bytes = []
+        self.byteString = ""
+        self.error = [MeasurementError.OK]
+        self.counter = 0
+
+        //only used for sorting purposes
+        self.idValue = idValue
+
+    }
+
+    ///
+    /// - parameter bytes:  raw data bytes as read from the sensor
+    /// - parameter slope:  slope to calculate glucose from raw value in (mg/dl)/raw
+    /// - parameter offset: glucose offset to be added in mg/dl
+    /// - parameter date:   date of the measurement
+    ///
+    /// - returns: Measurement
+    init(bytes: [UInt8], slope: Double = 0.1, offset: Double = 0.0, counter: Int = 0, date: Date, idValue: Int = 0) {
+        self.bytes = bytes
+        self.byteString = bytes.reduce("", { $0 + String(format: "%02X", arguments: [$1]) })
+        //self.rawGlucose = (Int(bytes[1] & 0x1F) << 8) + Int(bytes[0]) // switched to 13 bit mask on 2018-03-15
+        self.rawGlucose = SensorData.readBits(bytes, 0, 0, 0xe)
+
+        //self.rawTemperature = (Int(bytes[4] & 0x3F) << 8) + Int(bytes[3]) // 14 bit-mask for raw temperature
+        //raw temperature in libre FRAM is always stored in multiples of four
+        self.rawTemperature = SensorData.readBits(bytes, 0, 0x1a, 0xc) << 2
+
+        let temperatureAdjustment = (SensorData.readBits(bytes, 0, 0x26, 0x9) << 2)
+        let negativeAdjustment = SensorData.readBits(bytes, 0, 0x2f, 0x1) != 0
+        self.rawTemperatureAdjustment = negativeAdjustment ? -temperatureAdjustment : temperatureAdjustment
+
+        self.date = date
+        self.counter = counter
+
+        let errorBitField = SensorData.readBits(bytes,0, 0xe, 0xc)
+        self.error = Self.extractErrorBitField(errorBitField)
+
+        //only used for sorting purposes
+        self.idValue = idValue
+
+    }
+
+    static func extractErrorBitField(_ errBitField: Int) -> [MeasurementError]{
+        errBitField == 0 ?
+            [MeasurementError.OK] :
+            MeasurementError.allErrorCodes.filter { (errBitField & $0.rawValue) != 0}
+
+    }
+
+
+    var description: String {
+        String(" date:  \(date), rawGlucose: \(rawGlucose), rawTemperature: \(rawTemperature), bytes: \(bytes) \n")
+
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 785 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/PreLibre2.swift


+ 179 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/Sensor.swift

@@ -0,0 +1,179 @@
+import Foundation
+
+public extension UserDefaults {
+    private enum Key: String {
+        case sensor = "no.bjorninge.libre2sensor"
+        case calibrationMapping = "no.bjorninge.libre2sensor-calibrationmapping"
+
+
+    }
+
+    var preSelectedSensor: Sensor? {
+        get {
+
+            if let sensor = object(forKey: Key.sensor.rawValue) as? Data {
+                let decoder = JSONDecoder()
+                return try? decoder.decode(Sensor.self, from: sensor)
+            }
+
+            return nil
+
+        }
+        set {
+            if let newValue = newValue {
+                let encoder = JSONEncoder()
+                if let encoded = try? encoder.encode(newValue) {
+                    set(encoded, forKey: Key.sensor.rawValue)
+                }
+            } else {
+                removeObject(forKey: Key.sensor.rawValue)
+            }
+        }
+    }
+
+    var calibrationMapping : CalibrationToSensorMapping? {
+        get {
+            if let sensor = object(forKey: Key.calibrationMapping.rawValue) as? Data {
+                let decoder = JSONDecoder()
+                return try? decoder.decode(CalibrationToSensorMapping.self, from: sensor)
+            }
+
+            return nil
+
+        }
+        set {
+            if let newValue = newValue {
+                let encoder = JSONEncoder()
+                if let encoded = try? encoder.encode(newValue) {
+                    set(encoded, forKey: Key.calibrationMapping.rawValue)
+                }
+            } else {
+                removeObject(forKey: Key.calibrationMapping.rawValue)
+            }
+        }
+    }
+}
+
+public struct CalibrationToSensorMapping: Codable {
+    public let uuid: Data
+    public let reverseFooterCRC: Int
+
+    public init(uuid: Data, reverseFooterCRC: Int) {
+        self.uuid = uuid
+        self.reverseFooterCRC = reverseFooterCRC
+    }
+}
+
+
+public struct Sensor: Codable {
+    public let uuid: Data
+    public let patchInfo: Data
+   // public let calibrationInfo: SensorData.CalibrationInfo
+
+    //public let family: SensorFamily
+    //public let type: SensorType
+    //public let region: SensorRegion
+    //public let serial: String?
+    //public var state: SensorState
+    public var age: Int? = nil
+    public var maxAge: Int
+   // public var lifetime: Int
+
+    public var unlockCount: Int
+
+    /*
+    public var unlockCount: Int {
+        get {
+            return UserDefaults.standard.integer(forKey: Key.sensorUnlockCount.rawValue)
+        }
+        set {
+            UserDefaults.standard.setValue(newValue, forKey: Key.sensorUnlockCount.rawValue)
+        }
+    }*/
+
+    /*
+    public var elapsedLifetime: Int? {
+        get {
+            if let remainingLifetime = remainingLifetime {
+                return max(0, lifetime - remainingLifetime)
+            }
+
+            return nil
+        }
+    }
+
+    public var remainingLifetime: Int? {
+        get {
+            if let age = age {
+                return max(0, lifetime - age)
+            }
+
+            return nil
+        }
+    } */
+
+    public init(uuid: Data, patchInfo: Data, maxAge:Int, unlockCount: Int = 0) {
+        self.uuid = uuid
+        self.patchInfo = patchInfo
+
+        //self.family = SensorFamily(patchInfo: patchInfo)
+        //self.type = SensorType(patchInfo: patchInfo)
+        //self.region = SensorRegion(patchInfo: patchInfo)
+        //self.serial = sensorSerialNumber(sensorUID: self.uuid, sensorFamily: self.family)
+        //self.state = SensorState(fram: fram)
+        //self.lifetime = Int(fram[327]) << 8 + Int(fram[326])
+        self.unlockCount = 0
+        self.maxAge = maxAge
+        //self.calibrationInfo = calibrationInfo
+    }
+
+    public var description: String {
+        return [
+            "uuid: (\(uuid.hex))",
+            "patchInfo: (\(patchInfo.hex))",
+            //"calibrationInfo: (\(calibrationInfo.description))",
+            //"family: \(family.description)",
+            //"type: \(type.description)",
+            //"region: \(region.description)",
+            //"serial: \(serial ?? "Unknown")",
+            //"state: \(state.description)",
+            //"lifetime: \(lifetime.inTime)",
+        ].joined(separator: ", ")
+    }
+}
+
+fileprivate enum Key: String, CaseIterable {
+    case sensorUnlockCount = "libre-direct.settings.sensor.unlockCount"
+}
+
+/*
+fileprivate func sensorSerialNumber(sensorUID: Data, sensorFamily: SensorFamily) -> String? {
+    let lookupTable = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "T", "U", "V", "W", "X", "Y", "Z"]
+
+    guard sensorUID.count == 8 else {
+        return nil
+    }
+
+    let bytes = Array(sensorUID.reversed().suffix(6))
+    var fiveBitsArray = [UInt8]()
+    fiveBitsArray.append(bytes[0] >> 3)
+    fiveBitsArray.append(bytes[0] << 2 + bytes[1] >> 6)
+
+    fiveBitsArray.append(bytes[1] >> 1)
+    fiveBitsArray.append(bytes[1] << 4 + bytes[2] >> 4)
+
+    fiveBitsArray.append(bytes[2] << 1 + bytes[3] >> 7)
+
+    fiveBitsArray.append(bytes[3] >> 2)
+    fiveBitsArray.append(bytes[3] << 3 + bytes[4] >> 5)
+
+    fiveBitsArray.append(bytes[4])
+
+    fiveBitsArray.append(bytes[5] >> 3)
+    fiveBitsArray.append(bytes[5] << 2)
+
+    return fiveBitsArray.reduce("\(sensorFamily.rawValue)", {
+        $0 + lookupTable[Int(0x1F & $1)]
+    })
+}
+ */

Разница между файлами не показана из-за своего большого размера
+ 469 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorData.swift


+ 107 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorSerialNumber.swift

@@ -0,0 +1,107 @@
+//
+//  FreestyleLibreSensor.swift
+//  LibreMonitor
+//
+//  Created by Uwe Petersen on 01.04.18.
+//  Copyright © 2018 Uwe Petersen. All rights reserved.
+//
+
+import Foundation
+
+public struct SensorSerialNumber: CustomStringConvertible {
+    let uid: Data
+
+    fileprivate let lookupTable = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "T", "U", "V", "W", "X", "Y", "Z"]
+
+    init?(withUID uid: Data) {
+        guard uid.count == 8 else { return nil }
+        self.uid = uid
+    }
+
+    // MARK: - computed properties
+
+    var serialNumber: String {
+        // The serial number of the sensor can be derived from its uid.
+        //
+        // The numbers an letters of the serial number are coded a compressed scheme that uses only 32 numbers and letters,
+        // by omitting the letters B, I, O and S. This information is stored in consecutive units of five bits.
+        //
+        // The encoding thus is as follows:
+        //   index: 0 1 2 3 4 5 6 7 8 9 10     11 12 13 14 15 16     17 18 19 20 21     22 23 24      25 26 27 28 29 30 31
+        //   char:  0 1 2 3 4 5 6 7 8 9  A (B)  C  D  E  F  G  H (I)  J  K  L  M  N (O)  P  Q  R (S)   T  U  V  W  X  Y  Z
+        //
+        // Example:  75 ce 86 00 00 a0 07 e0
+        //    Uid is E0 07 A0 00 00 25 90 5E, and the corresponding serial number is "0M00009DHCR"
+        //           \   / \              /
+        //            -+-   -----+--------
+        //             |         |
+        //             |         +-- This part encodes the serial number, see below
+        //             +-- Standard first two bytes, where 0x07 is the code for "Texas Instruments Tag-it™", see https://en.wikipedia.org/wiki/ISO/IEC_15693
+        //
+        //   1.) Convert the part without E007, i.e. A0 00 00 25 90 5E to binary representation
+        //
+        //            A    0     0    0     0    0     2    5     9    0     5    E
+        //          1010 0000  0000 0000  0000 0000  0010 0101  1001 0000  0101 1110
+        //
+        //   2.) Split this binary array in units of five bits length from the beginning and pad with two zeros at the end and
+        //       calculate the corresponding integer and retreive the corresponding char from the table above
+        //
+        // Byte #       0           1          2          3          4          5
+        // Bit  #   8765 4321  8765 4321  8765 4321  8765 4321  8765 4321  8765 4321
+        //
+        //     +--  1010 0000  0000 0000  0000 0000  0010 0101  1001 0000  0101 1110  + 00
+        //     |    \    /\     /\    /\     / \     /\    /\     /\    /  \    /\       /
+        //     +->  10100  00000  00000 00000   00000  01001 01100  10000   01011 11000
+        //            |      |      |      |      |      |      |      |      |      |
+        //            |      |      |      |      |      |      |      |      |      +- = 24 -> "R" (Byte 6) << 2                  Mask 0x1F
+        //            |      |      |      |      |      |      |      |      +-------- = 11 -> "C" (Byte 5) >> 3                  Mask 0x1F
+        //            |      |      |      |      |      |      |      +--------------- = 16 -> "H" (Byte 4)                       Mask 0x1F
+        //            |      |      |      |      |      |      +---------------------- = 12 -> "D" (Byte 3) << 2 + (Byte 4) >> 5  Mask 0x1F
+        //            |      |      |      |      |      +----------------------------- =  9 -> "9" (Byte 3) >> 2                  Mask 0x1F
+        //            |      |      |      |      +------------------------------------ =  0 -> "0" (Byte 2) << 1 + (Byte 3) >> 7  Mask 0x1F
+        //            |      |      |      +------------------------------------------- =  0 -> "0" (Byte 1) << 4 + (Byte 2) >> 4  Mask 0x1F
+        //            |      |      +-------------------------------------------------- =  0 -> "0" (Byte 1) >> 1                  Mask 0x1F
+        //            |      +--------------------------------------------------------- =  0 -> "0" (Byte 0) << 2 + (Byte 1) >> 6  Mask 0x1F
+        //            +---------------------------------------------------------------- = 20 -> "M" (byte 0) >> 3                  Mask 0x1F
+        //
+        //
+        //   3.) Prepend "0" at the beginning an thus receive "0M00009DHCR"
+
+        guard uid.count == 8 else { return "invalid uid" }
+
+        let bytes = Array(uid.reversed().suffix(6))  // 5E 90 25 00 00 A0 07 E0" -> E0 07 A0 00 00 25 90 5E -> A0 00 00 25 90 5E
+
+        //  A0 00 00 25 90 5E -> "M00009DHCR"
+        var fiveBitsArray = [UInt8]() // Mask later with 0x1F to use only five bits
+
+        fiveBitsArray.append( bytes[0] >> 3 )
+        fiveBitsArray.append( bytes[0] << 2 + bytes[1] >> 6 )
+        fiveBitsArray.append( bytes[1] >> 1 )
+        fiveBitsArray.append( bytes[1] << 4 + bytes[2] >> 4 )
+        fiveBitsArray.append( bytes[2] << 1 + bytes[3] >> 7 )
+        fiveBitsArray.append( bytes[3] >> 2 )
+        fiveBitsArray.append( bytes[3] << 3 + bytes[4] >> 5 )
+        fiveBitsArray.append( bytes[4] )
+        fiveBitsArray.append( bytes[5] >> 3 )
+        fiveBitsArray.append( bytes[5] << 2 )
+
+        let serialNumber = fiveBitsArray.reduce("0", { // prepend with "0" according to step 3.)
+            $0 + lookupTable[ Int(0x1F & $1) ]  // Mask with 0x1F to only take the five relevant bits
+        })
+        return serialNumber
+    }
+
+    var uidString: String {
+        Data(self.uid).hexEncodedString()
+    }
+
+    var prettyUidString: String {
+        let stringArray = self.uid.map({ String(format: "%02X", $0) })
+        return stringArray.dropFirst().reduce(stringArray.first!, { $0 + ":" + $1 })
+    }
+
+    // MARK: - CustomStringConvertible Protocoll
+    public var description: String {
+        "Uid is \(prettyUidString) and derived serial number is \(serialNumber)"
+    }
+}

+ 69 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreSensor/SensorContents/SensorState.swift

@@ -0,0 +1,69 @@
+//
+//  SensorState.swift
+//  LibreMonitor
+//
+//  Created by Uwe Petersen on 31.07.16.
+//  Copyright © 2016 Uwe Petersen. All rights reserved.
+//
+
+import Foundation
+
+/// State of the freestyle libre sensor
+///
+/// - notYetStarted: 0x01 sensor not yet started
+/// - starting:      0x02 sensor is in the starting phase
+/// - ready:         0x03 sensor is ready, i.e. in normal operation mode
+/// - stateFour:     0x04 state with yet unknown meaning
+/// - expired:       0x05 sensor is expired
+/// - failure:       0x06 sensor has an error
+/// - unknown:       any other state
+enum SensorState {
+    case notYetStarted
+    case starting
+    case ready
+    case expired
+    case shutdown
+    case failure
+    case unknown
+
+    init() {
+        self = .unknown
+    }
+    init(stateByte: UInt8) {
+        switch stateByte {
+        case 01:
+            self = .notYetStarted
+        case 02:
+            self = .starting
+        case 03:
+            self = .ready
+        case 04:
+            self = .expired
+        case 05:
+            self = .shutdown
+        case 06:
+            self = .failure
+        default:
+            self = .unknown
+        }
+    }
+
+    var description: String {
+        switch self {
+        case .notYetStarted:
+            return "Sensor not yet startet"
+        case .starting:
+            return "Sensor in starting phase"
+        case .ready:
+            return "Sensor is ready"
+        case .expired:
+            return "Sensor is expired"
+        case .shutdown:
+            return "Sensor is shut down"
+        case .failure:
+            return "Sensor has failure"
+        default:
+            return "Unknown sensor state"
+        }
+    }
+}

+ 878 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterManager.swift

@@ -0,0 +1,878 @@
+//
+//  LibreTransmitterManager.swift
+//  Created by Bjørn Inge Berg on 25/02/2019.
+//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
+//
+import Foundation
+import UserNotifications
+import Combine
+import UIKit
+import CoreBluetooth
+import HealthKit
+import os.log
+
+public protocol LibreTransmitterManagerDelegate: AnyObject {
+    var queue: DispatchQueue { get }
+
+    func startDateToFilterNewData(for: LibreTransmitterManager) -> Date?
+
+    func cgmManager(_:LibreTransmitterManager, hasNew result: Result<[LibreGlucose], Error>)
+
+    func overcalibration(for: LibreTransmitterManager) -> ((Double) -> (Double))?
+}
+
+public final class LibreTransmitterManager: LibreTransmitterDelegate {
+
+    public typealias GlucoseArrayWithPrediction = (glucose:[LibreGlucose], prediction:[LibreGlucose])
+    public lazy var logger = Logger(forType: Self.self)
+
+
+    public let isOnboarded = true   // No distinction between created and onboarded
+    public var hasValidSensorSession: Bool {
+        lastConnected != nil
+    }
+
+    public var glucoseDisplay: GlucoseDisplayable?
+    public var trend: GlucoseTrend?
+
+
+    public func libreManagerDidRestoreState(found peripherals: [CBPeripheral], connected to: CBPeripheral?) {
+        let devicename = to?.name  ?? "no device"
+        let id = to?.identifier.uuidString ?? "null"
+        let msg = String(format: NSLocalizedString("Bluetooth State restored (APS restarted?). Found %d peripherals, and connected to %@ with identifier %@", comment: "Restored state message"), peripherals.count, devicename, id)
+
+        NotificationHelper.sendRestoredStateNotification(msg: msg)
+    }
+
+    public var batteryLevel: Double? {
+        let batt = self.proxy?.metadata?.battery
+        logger.debug("dabear:: LibreTransmitterManager was asked to return battery: \(batt.debugDescription)")
+        //convert from 8% -> 0.8
+        if let battery = proxy?.metadata?.battery {
+            return Double(battery) / 100
+        }
+
+        return nil
+    }
+
+
+
+    public var cgmManagerDelegate: LibreTransmitterManagerDelegate? {
+        get {
+            return delegate.delegate
+        }
+        set {
+            delegate.delegate = newValue
+        }
+    }
+
+    public var delegateQueue: DispatchQueue! {
+        get {
+            return delegate.queue
+        }
+        set {
+            delegate.queue = newValue
+        }
+    }
+
+    public let delegate = WeakSynchronizedDelegate<LibreTransmitterManagerDelegate>()
+
+    public var managedDataInterval: TimeInterval?
+
+
+    private func getPersistedSensorDataForDebug() -> String {
+        guard let data = UserDefaults.standard.queuedSensorData else {
+            return "nil"
+        }
+
+        let c = self.calibrationData?.description ?? "no calibrationdata"
+        return data.array.map {
+            "SensorData(uuid: \"0123\".data(using: .ascii)!, bytes: \($0.bytes))!"
+        }
+        .joined(separator: ",\n")
+        + ",\n Calibrationdata: \(c)"
+    }
+
+    public var debugDescription: String {
+
+        return [
+            "## LibreTransmitterManager",
+            "Testdata: foo",
+            "lastConnected: \(String(describing: lastConnected))",
+            "Connection state: \(connectionState)",
+            "Sensor state: \(sensorStateDescription)",
+            "transmitterbattery: \(batteryString)",
+            "SensorData: \(getPersistedSensorDataForDebug())",
+            "Metainfo::\n\(AppMetaData.allProperties)",
+            ""
+        ].joined(separator: "\n")
+    }
+
+    public private(set) var lastConnected: Date?
+
+    public private(set) var alarmStatus = AlarmStatus()
+
+
+    public private(set) var latestPrediction: LibreGlucose?
+    public private(set) var latestBackfill: LibreGlucose? {
+        willSet(newValue) {
+            guard let newValue = newValue else {
+                return
+            }
+
+            var trend: GlucoseTrend?
+            let oldValue = latestBackfill
+
+            defer {
+                logger.debug("dabear:: sending glucose notification")
+                NotificationHelper.sendGlucoseNotitifcationIfNeeded(glucose: newValue,
+                                                                    oldValue: oldValue,
+                                                                    trend: trend,
+                                                                    battery: batteryString)
+
+                //once we have a new glucose value, we can update the isalarming property
+                if let activeAlarms = UserDefaults.standard.glucoseSchedules?.getActiveAlarms(newValue.glucoseDouble) {
+                    DispatchQueue.main.async {
+                        self.alarmStatus.isAlarming = ([.high,.low].contains(activeAlarms))
+                        self.alarmStatus.glucoseScheduleAlarmResult = activeAlarms
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                    self.alarmStatus.isAlarming = false
+                    self.alarmStatus.glucoseScheduleAlarmResult = .none
+                    }
+                }
+
+
+            }
+
+            logger.debug("dabear:: latestBackfill set, newvalue is \(newValue.description)")
+
+            if let oldValue = oldValue {
+                // the idea here is to use the diff between the old and the new glucose to calculate slope and direction, rather than using trend from the glucose value.
+                // this is because the old and new glucose values represent earlier readouts, while the trend buffer contains somewhat more jumpy (noisy) values.
+                let timediff = LibreGlucose.timeDifference(oldGlucose: oldValue, newGlucose: newValue)
+                logger.debug("dabear:: timediff is \(timediff)")
+                let oldIsRecentEnough = timediff <= TimeInterval.minutes(15)
+
+                trend = oldIsRecentEnough ? newValue.GetGlucoseTrend(last: oldValue) : nil
+
+                var batteries : [(name: String, percentage: Int)]?
+                if let metaData = metaData, let battery = battery {
+                    batteries = [(name: metaData.name, percentage: battery)]
+                }
+
+                self.glucoseDisplay = ConcreteGlucoseDisplayable(isStateValid: newValue.isStateValid, trendType: trend, isLocal: true, batteries: batteries)
+            } else {
+                //could consider setting this to ConcreteSensorDisplayable with trendtype GlucoseTrend.flat, but that would be kinda lying
+                self.glucoseDisplay = nil
+            }
+        }
+
+    }
+
+    static public var managerIdentifier : String {
+        Self.className
+    }
+
+    static public let localizedTitle = LocalizedString("Libre Bluetooth", comment: "Title for the CGMManager option")
+
+
+
+    public init() {
+        lastConnected = nil
+        //let isui = (self is CGMManagerUI)
+        //self.miaomiaoService = MiaomiaoService(keychainManager: keychain)
+        logger.debug("dabear: LibreTransmitterManager will be created now")
+        //proxy = MiaoMiaoBluetoothManager()
+        proxy?.delegate = self
+    }
+
+    public func disconnect() {
+        logger.debug("dabear:: LibreTransmitterManager disconnect called")
+
+        proxy?.disconnectManually()
+        proxy?.delegate = nil
+    }
+
+    deinit {
+        logger.debug("dabear:: LibreTransmitterManager deinit called")
+        //cleanup any references to events to this class
+        disconnect()
+    }
+
+    //lazy because we don't want to scan immediately
+    private lazy var proxy: LibreTransmitterProxyManager? = LibreTransmitterProxyManager()
+
+    /*
+     These properties are mostly useful for swiftui
+     */
+    public var transmitterInfoObservable = TransmitterInfo()
+    public var sensorInfoObservable = SensorInfo()
+    public var glucoseInfoObservable = GlucoseInfo()
+
+
+
+    var longDateFormatter : DateFormatter = ({
+        let df = DateFormatter()
+        df.dateStyle = .long
+        df.timeStyle = .long
+        df.doesRelativeDateFormatting = true
+        return df
+    })()
+
+    var dateFormatter : DateFormatter = ({
+        let df = DateFormatter()
+        df.dateStyle = .long
+        df.timeStyle = .full
+        df.locale = Locale.current
+        return df
+    })()
+
+
+    //when was the libre2 direct ble update last received?
+    var lastDirectUpdate : Date? = nil
+
+    private var countTimesWithoutData: Int = 0
+
+
+}
+
+
+// MARK: - Convenience functions
+extension LibreTransmitterManager {
+    func setObservables(sensorData: SensorData?, bleData: Libre2.LibreBLEResponse?, metaData: LibreTransmitterMetadata?) {
+        logger.debug("dabear:: setObservables called")
+        DispatchQueue.main.async {
+
+
+            if let metaData=metaData {
+                self.logger.debug("dabear::will set transmitterInfoObservable")
+                self.transmitterInfoObservable.battery = metaData.batteryString
+                self.transmitterInfoObservable.hardware = metaData.hardware
+                self.transmitterInfoObservable.firmware = metaData.firmware
+                self.transmitterInfoObservable.sensorType = metaData.sensorType()?.description ?? "Unknown"
+                self.transmitterInfoObservable.transmitterIdentifier = metaData.macAddress ??  UserDefaults.standard.preSelectedDevice ?? "Unknown"
+
+
+            }
+
+            self.transmitterInfoObservable.connectionState = self.proxy?.connectionStateString ?? "n/a"
+            self.transmitterInfoObservable.transmitterType = self.proxy?.shortTransmitterName ?? "Unknown"
+
+            if let sensorData = sensorData {
+                self.logger.debug("dabear::will set sensorInfoObservable")
+                self.sensorInfoObservable.sensorAge = sensorData.humanReadableSensorAge
+                self.sensorInfoObservable.sensorAgeLeft = sensorData.humanReadableTimeLeft
+
+                self.sensorInfoObservable.sensorState = sensorData.state.description
+                self.sensorInfoObservable.sensorSerial = sensorData.serialNumber
+
+                self.glucoseInfoObservable.checksum = String(sensorData.footerCrc.byteSwapped)
+
+
+
+                if let sensorEndTime = sensorData.sensorEndTime {
+                    self.sensorInfoObservable.sensorEndTime = self.dateFormatter.string(from: sensorEndTime )
+
+                } else {
+                    self.sensorInfoObservable.sensorEndTime = "Unknown or ended"
+
+                }
+
+            } else if let bleData = bleData, let sensor = UserDefaults.standard.preSelectedSensor {
+                let aday = 86_400.0 //in seconds
+                var humanReadableSensorAge: String {
+                    let days = TimeInterval(bleData.age * 60) / aday
+                    return String(format: "%.2f", days) + " day(s)"
+                }
+
+
+                var maxMinutesWearTime : Int{
+                    sensor.maxAge
+                }
+
+                var minutesLeft: Int {
+                    maxMinutesWearTime - bleData.age
+                }
+
+
+                var humanReadableTimeLeft: String {
+                    let days = TimeInterval(minutesLeft * 60) / aday
+                    return String(format: "%.2f", days) + " day(s)"
+                }
+
+                //once the sensor has ended we don't know the exact date anymore
+                var sensorEndTime: Date? {
+                    if minutesLeft <= 0 {
+                        return nil
+                    }
+
+                    // we can assume that the libre2 direct bluetooth packet is received immediately
+                    // after the sensor has been done a new measurement, so using Date() should be fine here
+                    return Date().addingTimeInterval(TimeInterval(minutes: Double(minutesLeft)))
+                }
+
+                self.sensorInfoObservable.sensorAge = humanReadableSensorAge
+                self.sensorInfoObservable.sensorAgeLeft = humanReadableTimeLeft
+                self.sensorInfoObservable.sensorState = "Operational"
+                self.sensorInfoObservable.sensorState = "Operational"
+                self.sensorInfoObservable.sensorSerial = SensorSerialNumber(withUID: sensor.uuid)?.serialNumber ?? "-"
+
+                if let mapping = UserDefaults.standard.calibrationMapping,
+                   let calibration = self.calibrationData ,
+                   mapping.uuid == sensor.uuid && calibration.isValidForFooterWithReverseCRCs ==  mapping.reverseFooterCRC {
+                    self.glucoseInfoObservable.checksum = "\(mapping.reverseFooterCRC)"
+                }
+
+                if let sensorEndTime = sensorEndTime {
+                    self.sensorInfoObservable.sensorEndTime = self.dateFormatter.string(from: sensorEndTime )
+
+                } else {
+                    self.sensorInfoObservable.sensorEndTime = "Unknown or ended"
+
+                }
+
+            }
+
+            let formatter = QuantityFormatter()
+            let preferredUnit = UserDefaults.standard.mmGlucoseUnit ?? .millimolesPerLiter
+
+
+            if let d = self.latestBackfill {
+                self.logger.debug("dabear::will set glucoseInfoObservable")
+
+                formatter.setPreferredNumberFormatter(for: .millimolesPerLiter)
+                self.glucoseInfoObservable.glucoseMMOL = formatter.string(from: d.quantity, for: .millimolesPerLiter) ?? "-"
+
+
+                formatter.setPreferredNumberFormatter(for: .milligramsPerDeciliter)
+                self.glucoseInfoObservable.glucoseMGDL = formatter.string(from: d.quantity, for: .milligramsPerDeciliter) ?? "-"
+
+                //backward compat
+                if preferredUnit == .millimolesPerLiter {
+                    self.glucoseInfoObservable.glucose = self.glucoseInfoObservable.glucoseMMOL
+                } else if preferredUnit == .milligramsPerDeciliter {
+                    self.glucoseInfoObservable.glucose = self.glucoseInfoObservable.glucoseMGDL
+                }
+
+
+
+                self.glucoseInfoObservable.date = self.longDateFormatter.string(from: d.timestamp)
+            }
+
+            if let d = self.latestPrediction {
+                formatter.setPreferredNumberFormatter(for: .millimolesPerLiter)
+                self.glucoseInfoObservable.predictionMMOL = formatter.string(from: d.quantity, for: .millimolesPerLiter) ?? "-"
+
+
+                formatter.setPreferredNumberFormatter(for: .milligramsPerDeciliter)
+                self.glucoseInfoObservable.predictionMGDL = formatter.string(from: d.quantity, for: .milligramsPerDeciliter) ?? "-"
+                self.glucoseInfoObservable.predictionDate = self.longDateFormatter.string(from: d.timestamp)
+
+
+            }
+
+
+
+
+
+        }
+
+
+    }
+
+    func getStartDateForFilter() -> Date?{
+        var startDate: Date?
+
+        self.delegateQueue.sync {
+            startDate = self.cgmManagerDelegate?.startDateToFilterNewData(for: self) ?? self.latestBackfill?.startDate
+        }
+
+        // add one second to startdate to make this an exclusive (non overlapping) match
+        return startDate?.addingTimeInterval(1)
+    }
+
+    func glucosesToSamplesFilter(_ array: [LibreGlucose], startDate: Date?) -> [LibreGlucose] {
+        array
+            .filterDateRange(startDate, nil)
+            .filter { $0.isStateValid }
+            .compactMap { $0 }
+    }
+
+    public var calibrationData: SensorData.CalibrationInfo? {
+        KeychainManagerWrapper.standard.getLibreNativeCalibrationData()
+    }
+}
+
+
+// MARK: - Direct bluetooth updates
+extension LibreTransmitterManager {
+
+    public func libreSensorDidUpdate(with bleData: Libre2.LibreBLEResponse, and device: LibreTransmitterMetadata) {
+        self.logger.debug("dabear:: got sensordata: \(String(describing: bleData))")
+        let typeDesc = device.sensorType().debugDescription
+
+        let now = Date()
+        //only once per mins minute
+        let mins =  4.5
+        if let earlierplus = lastDirectUpdate?.addingTimeInterval(mins * 60), earlierplus >= now  {
+            logger.debug("last ble update was less than \(mins) minutes ago, aborting loop update")
+            return
+        }
+
+        logger.debug("Directly connected to libresensor of type \(typeDesc). Details:  \(device.description)")
+
+        guard let mapping = UserDefaults.standard.calibrationMapping,
+              let calibrationData = calibrationData,
+              let sensor = UserDefaults.standard.preSelectedSensor else {
+            logger.error("calibrationdata, sensor uid or mapping missing, could not continue")
+            self.delegateQueue.async {
+                self.cgmManagerDelegate?.cgmManager(self, hasNew: .failure(LibreError.noCalibrationData))
+            }
+            return
+        }
+
+        guard mapping.reverseFooterCRC == calibrationData.isValidForFooterWithReverseCRCs &&
+                mapping.uuid == sensor.uuid else {
+            logger.error("Calibrationdata was not correct for these bluetooth packets. This is a fatal error, we cannot calibrate without re-pairing")
+            self.delegateQueue.async {
+                self.cgmManagerDelegate?.cgmManager(self, hasNew: .failure(LibreError.noCalibrationData))
+            }
+            return
+        }
+
+        guard bleData.crcVerified else {
+            self.delegateQueue.async {
+                self.cgmManagerDelegate?.cgmManager(self, hasNew: .failure(LibreError.checksumValidationError))
+            }
+
+            logger.debug("did not get bledata with valid crcs")
+            return
+        }
+
+        if sensor.maxAge > 0 {
+            let minutesLeft = Double(sensor.maxAge - bleData.age)
+            NotificationHelper.sendSensorExpireAlertIfNeeded(minutesLeft: minutesLeft)
+
+        }
+
+
+//        let device = self.proxy?.device
+
+
+
+        let sortedTrends = bleData.trend.sorted{ $0.date > $1.date}
+
+        let glucose = LibreGlucose.fromTrendMeasurements(sortedTrends, nativeCalibrationData: calibrationData, returnAll: UserDefaults.standard.mmBackfillFromTrend)
+        //glucose += LibreGlucose.fromHistoryMeasurements(bleData.history, nativeCalibrationData: calibrationData)
+        // while libre2 fram scans contains historymeasurements for the last 8 hours,
+        // history from bledata contains just a couple of data points, so we don't bother
+        /*if UserDefaults.standard.mmBackfillFromHistory {
+            let sortedHistory = bleData.history.sorted{ $0.date > $1.date}
+            glucose += LibreGlucose.fromHistoryMeasurements(sortedHistory, nativeCalibrationData: calibrationData)
+        }*/
+
+        let newGlucose = glucosesToSamplesFilter(glucose, startDate: getStartDateForFilter())
+
+        if newGlucose.isEmpty {
+            self.countTimesWithoutData &+= 1
+        } else {
+            self.latestBackfill = glucose.max { $0.startDate < $1.startDate }
+            self.logger.debug("dabear:: latestbackfill set to \(self.latestBackfill.debugDescription)")
+            self.countTimesWithoutData = 0
+        }
+
+        //todo: predictions also for libre2 bluetooth data
+        //self.latestPrediction = prediction?.first
+        self.setObservables(sensorData: nil, bleData: bleData, metaData: device)
+
+        self.logger.debug("dabear:: handleGoodReading returned with \(newGlucose.count) entries")
+        self.delegateQueue.async {
+            var result: Result<[LibreGlucose], Error>
+            // If several readings from a valid and running sensor come out empty,
+            // we have (with a large degree of confidence) a sensor that has been
+            // ripped off the body
+            if self.countTimesWithoutData > 1 {
+                result = .failure(LibreError.noValidSensorData)
+            } else {
+                result = .success(newGlucose)
+            }
+            self.cgmManagerDelegate?.cgmManager(self, hasNew: result)
+        }
+
+        lastDirectUpdate = Date()
+
+    }
+}
+
+// MARK: - Bluetooth transmitter data
+extension LibreTransmitterManager {
+
+    public func noLibreTransmitterSelected() {
+        NotificationHelper.sendNoTransmitterSelectedNotification()
+    }
+
+    public func libreTransmitterDidUpdate(with sensorData: SensorData, and device: LibreTransmitterMetadata) {
+
+        self.logger.debug("dabear:: got sensordata: \(String(describing: sensorData)), bytescount: \( sensorData.bytes.count), bytes: \(sensorData.bytes)")
+        var sensorData = sensorData
+
+        NotificationHelper.sendLowBatteryNotificationIfNeeded(device: device)
+        self.setObservables(sensorData: sensorData, bleData: nil, metaData: device)
+
+         if !sensorData.isLikelyLibre1FRAM {
+            if let patchInfo = device.patchInfo, let sensorType = SensorType(patchInfo: patchInfo) {
+                let needsDecryption = [SensorType.libre2, .libreUS14day].contains(sensorType)
+                if needsDecryption, let uid = device.uid {
+                    sensorData.decrypt(patchInfo: patchInfo, uid: uid)
+                }
+            } else {
+                logger.debug("Sensor type was incorrect, and no decryption of sensor was possible")
+                self.cgmManagerDelegate?.cgmManager(self, hasNew: .failure(LibreError.encryptedSensor))
+                return
+            }
+        }
+
+        let typeDesc = device.sensorType().debugDescription
+
+        logger.debug("Transmitter connected to libresensor of type \(typeDesc). Details:  \(device.description)")
+
+        tryPersistSensorData(with: sensorData)
+
+        NotificationHelper.sendInvalidSensorNotificationIfNeeded(sensorData: sensorData)
+        NotificationHelper.sendInvalidChecksumIfDeveloper(sensorData)
+
+
+
+        guard sensorData.hasValidCRCs else {
+            self.delegateQueue.async {
+                self.cgmManagerDelegate?.cgmManager(self, hasNew: .failure(LibreError.checksumValidationError))
+            }
+
+            logger.debug("did not get sensordata with valid crcs")
+            return
+        }
+
+        NotificationHelper.sendSensorExpireAlertIfNeeded(sensorData: sensorData)
+
+        guard sensorData.state == .ready || sensorData.state == .starting else {
+            logger.debug("dabear:: got sensordata with valid crcs, but sensor is either expired or failed")
+            self.delegateQueue.async {
+                self.cgmManagerDelegate?.cgmManager(self, hasNew: .failure(LibreError.expiredSensor))
+            }
+            return
+        }
+
+        logger.debug("dabear:: got sensordata with valid crcs, sensor was ready")
+//        self.lastValidSensorData = sensorData
+
+
+        self.handleGoodReading(data: sensorData) { [weak self] error, glucoseArrayWithPrediction in
+            guard let self = self else {
+                print("dabear:: handleGoodReading could not lock on self, aborting")
+                return
+            }
+            if let error = error {
+                self.logger.error("dabear:: handleGoodReading returned with error: \(error.errorDescription)")
+                self.delegateQueue.async {
+                    self.cgmManagerDelegate?.cgmManager(self, hasNew: .failure(error))
+                }
+                return
+            }
+
+
+            guard let glucose = glucoseArrayWithPrediction?.glucose else {
+                self.logger.debug("dabear:: handleGoodReading returned with no data")
+                self.delegateQueue.async {
+                    self.cgmManagerDelegate?.cgmManager(self, hasNew: .success([]))
+                }
+                return
+            }
+
+            let prediction = glucoseArrayWithPrediction?.prediction
+
+
+
+//            let device = self.proxy?.device
+            let newGlucose = self.glucosesToSamplesFilter(glucose, startDate: self.getStartDateForFilter())
+
+
+
+            if newGlucose.isEmpty {
+                self.countTimesWithoutData &+= 1
+            } else {
+                self.latestBackfill = glucose.max { $0.startDate < $1.startDate }
+                self.logger.debug("dabear:: latestbackfill set to \(self.latestBackfill.debugDescription)")
+                self.countTimesWithoutData = 0
+            }
+
+            self.latestPrediction = prediction?.first
+
+            //must be inside this handler as setobservables "depend" on latestbackfill
+            self.setObservables(sensorData: sensorData, bleData: nil, metaData: nil)
+
+            self.logger.debug("dabear:: handleGoodReading returned with \(newGlucose.count) entries")
+            self.delegateQueue.async {
+                var result: Result<[LibreGlucose], Error>
+                // If several readings from a valid and running sensor come out empty,
+                // we have (with a large degree of confidence) a sensor that has been
+                // ripped off the body
+                if self.countTimesWithoutData > 1 {
+                    result = .failure(LibreError.noValidSensorData)
+                } else {
+                    result = .success(newGlucose)
+                }
+                self.cgmManagerDelegate?.cgmManager(self, hasNew: result)
+            }
+        }
+
+    }
+    private func readingToGlucose(_ data: SensorData, calibration: SensorData.CalibrationInfo) -> GlucoseArrayWithPrediction {
+
+        var entries: [LibreGlucose] = []
+        var prediction: [LibreGlucose] = []
+
+        let predictGlucose = true
+
+        // Increase to up to 15 to move closer to real blood sugar
+        // The cost is slightly more noise on consecutive readings
+        let glucosePredictionMinutes : Double = 10
+
+        if predictGlucose {
+            // We cheat here by forcing the loop to think that the predicted glucose value is the current blood sugar value.
+            logger.debug("Predicting glucose value")
+            if let predicted = data.predictBloodSugar(glucosePredictionMinutes){
+                let currentBg = predicted.roundedGlucoseValueFromRaw2(calibrationInfo: calibration)
+                let bgDate = predicted.date.addingTimeInterval(60 * -glucosePredictionMinutes)
+
+                prediction.append(LibreGlucose(unsmoothedGlucose: currentBg, glucoseDouble: currentBg, timestamp: bgDate))
+                logger.debug("Predicted glucose (not used) was: \(currentBg)")
+            } else {
+                logger.debug("Tried to predict glucose value but failed!")
+            }
+
+        }
+
+        let trends = data.trendMeasurements()
+        let firstTrend = trends.first?.roundedGlucoseValueFromRaw2(calibrationInfo: calibration)
+        logger.debug("first trend was: \(String(describing: firstTrend))")
+        entries = LibreGlucose.fromTrendMeasurements(trends, nativeCalibrationData: calibration, returnAll: UserDefaults.standard.mmBackfillFromTrend)
+
+        if UserDefaults.standard.mmBackfillFromHistory {
+            let history = data.historyMeasurements()
+            entries += LibreGlucose.fromHistoryMeasurements(history, nativeCalibrationData: calibration)
+        }
+
+        // overcalibrate
+        var overcalibration: ((Double) -> (Double))? = nil
+        delegateQueue.sync { overcalibration = cgmManagerDelegate?.overcalibration(for: self) }
+
+        if let overcalibration = overcalibration {
+            func overcalibrate(entries: [LibreGlucose]) -> [LibreGlucose] {
+                entries.map { entry in
+                    var entry = entry
+                    entry.glucoseDouble = overcalibration(entry.glucoseDouble)
+                    return entry
+                }
+            }
+
+            entries = overcalibrate(entries: entries)
+            prediction = overcalibrate(entries: prediction)
+        }
+
+        return (glucose: entries, prediction: prediction)
+    }
+
+    public func handleGoodReading(data: SensorData?, _ callback: @escaping (LibreError?, GlucoseArrayWithPrediction?) -> Void) {
+        //only care about the once per minute readings here, historical data will not be considered
+        guard let data = data else {
+            callback(.noSensorData, nil)
+            return
+        }
+
+
+        if let calibrationdata = calibrationData {
+            logger.debug("dabear:: calibrationdata loaded")
+
+            if calibrationdata.isValidForFooterWithReverseCRCs == data.footerCrc.byteSwapped {
+                logger.debug("dabear:: calibrationdata correct for this sensor, returning last values")
+
+                callback(nil, readingToGlucose(data, calibration: calibrationdata))
+                return
+            } else {
+                logger.debug("dabear:: calibrationdata incorrect for this sensor, calibrationdata.isValidForFooterWithReverseCRCs: \(calibrationdata.isValidForFooterWithReverseCRCs),  data.footerCrc.byteSwapped: \(data.footerCrc.byteSwapped)")
+            }
+        } else {
+            logger.debug("dabear:: calibrationdata was nil")
+        }
+
+        calibrateSensor(sensordata: data) { [weak self] calibrationparams  in
+            do {
+                try KeychainManagerWrapper.standard.setLibreNativeCalibrationData(calibrationparams)
+            } catch {
+                NotificationHelper.sendCalibrationNotification(.invalidCalibrationData)
+                callback(.invalidCalibrationData, nil)
+                return
+            }
+            //here we assume success, data is not changed,
+            //and we trust that the remote endpoint returns correct data for the sensor
+            NotificationHelper.sendCalibrationNotification(.success)
+            callback(nil, self?.readingToGlucose(data, calibration: calibrationparams))
+        }
+    }
+
+    //will be called on utility queue
+    public func libreTransmitterStateChanged(_ state: BluetoothmanagerState) {
+        DispatchQueue.main.async {
+            self.transmitterInfoObservable.connectionState = self.proxy?.connectionStateString ?? "n/a"
+            self.transmitterInfoObservable.transmitterType = self.proxy?.shortTransmitterName ?? "Unknown"
+        }
+        switch state {
+        case .Connected:
+            lastConnected = Date()
+        case .powerOff:
+            NotificationHelper.sendBluetoothPowerOffNotification()
+        default:
+            break
+        }
+        return
+    }
+
+    //will be called on utility queue
+    public func libreTransmitterReceivedMessage(_ messageIdentifier: UInt16, txFlags: UInt8, payloadData: Data) {
+        guard let packet = MiaoMiaoResponseState(rawValue: txFlags) else {
+            // Incomplete package?
+            // this would only happen if delegate is called manually with an unknown txFlags value
+            // this was the case for readouts that were not yet complete
+            // but that was commented out in MiaoMiaoManager.swift, see comment there:
+            // "dabear-edit: don't notify on incomplete readouts"
+            logger.debug("dabear:: incomplete package or unknown response state")
+            return
+        }
+
+        switch packet {
+        case .newSensor:
+            logger.debug("dabear:: new libresensor detected")
+            NotificationHelper.sendSensorChangeNotificationIfNeeded()
+        case .noSensor:
+            logger.debug("dabear:: no libresensor detected")
+            NotificationHelper.sendSensorNotDetectedNotificationIfNeeded(noSensor: true)
+        case .frequencyChangedResponse:
+            logger.debug("dabear:: transmitter readout interval has changed!")
+
+        default:
+            //we don't care about the rest!
+            break
+        }
+
+        return
+    }
+
+    func tryPersistSensorData(with sensorData: SensorData) {
+        guard UserDefaults.standard.shouldPersistSensorData else {
+            return
+        }
+
+        //yeah, we really really need to persist any changes right away
+        var data = UserDefaults.standard.queuedSensorData ?? LimitedQueue<SensorData>()
+        data.enqueue(sensorData)
+        UserDefaults.standard.queuedSensorData = data
+    }
+}
+
+// MARK: - conventience properties to access the enclosed proxy's properties
+extension LibreTransmitterManager {
+    public var device: HKDevice? {
+         //proxy?.OnQueue_device
+        proxy?.device
+    }
+
+    static var className: String {
+        String(describing: Self.self)
+    }
+    //cannot be called from managerQueue
+    public var identifier: String {
+        //proxy?.OnQueue_identifer?.uuidString ?? "n/a"
+        proxy?.identifier?.uuidString ?? "n/a"
+    }
+
+    public var metaData: LibreTransmitterMetadata? {
+        //proxy?.OnQueue_metadata
+         proxy?.metadata
+    }
+
+    //cannot be called from managerQueue
+    public var connectionState: String {
+        //proxy?.connectionStateString ?? "n/a"
+        proxy?.connectionStateString ?? "n/a"
+    }
+    //cannot be called from managerQueue
+    public var sensorSerialNumber: String {
+        //proxy?.OnQueue_sensorData?.serialNumber ?? "n/a"
+        proxy?.sensorData?.serialNumber ?? "n/a"
+    }
+
+    //cannot be called from managerQueue
+    public var sensorAge: String {
+        //proxy?.OnQueue_sensorData?.humanReadableSensorAge ?? "n/a"
+        proxy?.sensorData?.humanReadableSensorAge ?? "n/a"
+    }
+
+    public var sensorEndTime : String {
+        if let endtime = proxy?.sensorData?.sensorEndTime  {
+            let mydf = DateFormatter()
+            mydf.dateStyle = .long
+            mydf.timeStyle = .full
+            mydf.locale = Locale.current
+            return mydf.string(from: endtime)
+        }
+        return "Unknown or Ended"
+    }
+
+    public var sensorTimeLeft: String {
+        //proxy?.OnQueue_sensorData?.humanReadableSensorAge ?? "n/a"
+        proxy?.sensorData?.humanReadableTimeLeft ?? "n/a"
+    }
+
+    //cannot be called from managerQueue
+    public var sensorFooterChecksums: String {
+        //(proxy?.OnQueue_sensorData?.footerCrc.byteSwapped).map(String.init)
+        (proxy?.sensorData?.footerCrc.byteSwapped).map(String.init)
+
+            ?? "n/a"
+    }
+
+
+
+    //cannot be called from managerQueue
+    public var sensorStateDescription: String {
+        //proxy?.OnQueue_sensorData?.state.description ?? "n/a"
+        proxy?.sensorData?.state.description ?? "n/a"
+    }
+    //cannot be called from managerQueue
+    public var firmwareVersion: String {
+        proxy?.metadata?.firmware ?? "n/a"
+    }
+
+    //cannot be called from managerQueue
+    public var hardwareVersion: String {
+        proxy?.metadata?.hardware ?? "n/a"
+    }
+
+    //cannot be called from managerQueue
+    public var batteryString: String {
+        proxy?.metadata?.batteryString ?? "n/a"
+    }
+
+    public var battery: Int? {
+        proxy?.metadata?.battery
+    }
+
+    public func getDeviceType() -> String {
+        proxy?.shortTransmitterName ?? "Unknown"
+    }
+    public func getSmallImage() -> UIImage? {
+        proxy?.activePluginType?.smallImage ?? UIImage(named: "libresensor", in: Bundle.module, compatibleWith: nil)
+    }
+}

+ 115 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Controllers/LibreTransmitterSetupViewController.swift

@@ -0,0 +1,115 @@
+//
+//  MiaomiaoClientSetupViewController.swift
+//  Loop
+//
+//  Copyright © 2018 LoopKit Authors. All rights reserved.
+//
+
+import Combine
+import SwiftUI
+import UIKit
+import os.log
+import HealthKit
+
+public protocol CGMManagerSetupViewController {
+    var setupDelegate: CGMManagerSetupViewControllerDelegate? { get set }
+}
+
+public protocol CGMManagerSetupViewControllerDelegate: AnyObject {
+    func cgmManagerSetupViewController(_ cgmManagerSetupViewController: CGMManagerSetupViewController, didSetUpCGMManager cgmManager: LibreTransmitterManager)
+}
+
+class LibreTransmitterSetupViewController: UINavigationController,
+                                           CGMManagerSetupViewController,
+
+                                           CompletionNotifying {
+
+    weak var setupDelegate: CGMManagerSetupViewControllerDelegate?
+    weak var completionDelegate: CompletionDelegate?
+
+    var modeSelection: UIHostingController<ModeSelectionView>!
+
+    fileprivate var logger = Logger.init(subsystem: "no.bjorninge.libre", category: "LibreTransmitterSetupViewController")
+    lazy var cgmManager: LibreTransmitterManager? =  LibreTransmitterManager()
+
+
+    init() {
+        SelectionState.shared.selectedStringIdentifier = UserDefaults.standard.preSelectedDevice
+
+        let cancelNotifier = GenericObservableObject()
+        let saveNotifier = GenericObservableObject()
+
+        modeSelection = UIHostingController(rootView: ModeSelectionView(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier))
+
+
+        super.init(rootViewController: modeSelection)
+
+
+        cancelNotifier.listenOnce { [weak self] in
+            self?.cancel()
+        }
+
+        saveNotifier.listenOnce { [weak self] in
+            self?.save()
+        }
+
+    }
+
+    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
+        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
+    }
+
+    deinit {
+        logger.debug("dabear LibreTransmitterSetupViewController() deinit was called")
+        //cgmManager = nil
+    }
+
+    @available(*, unavailable)
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    @objc
+    private func cancel() {
+        completionDelegate?.completionNotifyingDidComplete(self)
+
+    }
+
+    @objc
+    private func save() {
+
+        let hasNewDevice = SelectionState.shared.selectedStringIdentifier != UserDefaults.standard.preSelectedDevice
+        if hasNewDevice, let newDevice = SelectionState.shared.selectedStringIdentifier {
+            logger.debug("dabear: Setupcontroller will set new device to \(newDevice)")
+            UserDefaults.standard.preSelectedDevice = newDevice
+            SelectionState.shared.selectedUID = nil
+            UserDefaults.standard.preSelectedUid = nil
+
+        } else if let newUID = SelectionState.shared.selectedUID {
+            // this one is only temporary,
+            // as we don't know the bluetooth identifier during nfc setup
+            logger.debug("dabear: Setupcontroller will set new libre2 device  to \(newUID)")
+
+            UserDefaults.standard.preSelectedUid = newUID
+            SelectionState.shared.selectedUID = nil
+            UserDefaults.standard.preSelectedDevice = nil
+
+
+        } else {
+
+            //this cannot really happen unless you are a developer and have previously
+            // stored both preSelectedDevice and selectedUID !
+        }
+
+        if let cgmManager = cgmManager {
+            logger.debug("dabear: Setupcontroller Saving from setup")
+            setupDelegate?.cgmManagerSetupViewController(self, didSetUpCGMManager: cgmManager)
+
+        } else {
+            logger.debug("dabear: Setupcontroller not Saving from setup")
+        }
+
+
+        completionDelegate?.completionNotifyingDidComplete(self)
+    }
+}

BIN
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Graphics/bubble.png


BIN
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Graphics/ic_bubble_mini_3-2.png


+ 0 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Graphics/icons8-down-arrow-50.png


Некоторые файлы не были показаны из-за большого количества измененных файлов