Przeglądaj źródła

test Dexcom support (hardcoded transmitter id!)

Ivan Valkou 4 lat temu
rodzic
commit
db3d029560

+ 6 - 2
Dependecies/CGMBLEKit/CGMBLEKit.xcodeproj/project.pbxproj

@@ -500,7 +500,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LOCALIZED_STRING_MACRO_NAMES = (
 					NSLocalizedString,
 					CFLocalizedString,
@@ -561,7 +561,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LOCALIZED_STRING_MACRO_NAMES = (
 					NSLocalizedString,
 					CFLocalizedString,
@@ -593,11 +593,13 @@
 				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
 				INFOPLIST_FILE = CGMBLEKit/Info.plist;
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.cgmblekit;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
+				SUPPORTS_MACCATALYST = NO;
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -617,11 +619,13 @@
 				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
 				INFOPLIST_FILE = CGMBLEKit/Info.plist;
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.cgmblekit;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
+				SUPPORTS_MACCATALYST = NO;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Release;

+ 19 - 15
Dependecies/CGMBLEKit/CGMBLEKit/TransmitterManager.swift

@@ -8,6 +8,11 @@
 import os.log
 import HealthKit
 
+public protocol TransmitterManagerDelegate: AnyObject {
+    func transmitterManager(_ manager: TransmitterManager, didRead glucose: [Glucose])
+    func transmitterManager(_ manager: TransmitterManager, didFailWith error: Error)
+}
+
 public struct TransmitterManagerState: Equatable {
 
     public static let version = 1
@@ -30,6 +35,8 @@ public protocol TransmitterManagerObserver: AnyObject {
 }
 
 public class TransmitterManager: TransmitterDelegate {
+    public weak var delegate: TransmitterManagerDelegate?
+
     private var state: TransmitterManagerState
 
     private let observers = WeakSynchronizedSet<TransmitterManagerObserver>()
@@ -155,6 +162,7 @@ public class TransmitterManager: TransmitterDelegate {
 
     public func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) {
         guard glucose != latestReading else {
+            delegate?.transmitterManager(self, didRead: [])
 //            updateDelegate(with: .noData)
             return
         }
@@ -165,17 +173,21 @@ public class TransmitterManager: TransmitterDelegate {
 
         guard glucose.state.hasReliableGlucose else {
             log.default("%{public}@: Unreliable glucose: %{public}@", #function, String(describing: glucose.state))
+            delegate?.transmitterManager(self, didFailWith: CalibrationError.unreliableState(glucose.state))
 //            updateDelegate(with: .error(CalibrationError.unreliableState(glucose.state)))
             return
         }
         
         guard let quantity = glucose.glucose else {
+            delegate?.transmitterManager(self, didRead: [])
 //            updateDelegate(with: .noData)
             return
         }
 
         log.default("%{public}@: New glucose", #function)
 
+        delegate?.transmitterManager(self, didRead: [glucose])
+
 //        updateDelegate(with: .newData([
 //            NewGlucoseSample(
 //                date: glucose.readDate,
@@ -190,21 +202,13 @@ public class TransmitterManager: TransmitterDelegate {
     }
 
     public func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) {
-//        let samples = glucose.compactMap { (glucose) -> NewGlucoseSample? in
-//            guard glucose != latestReading, glucose.state.hasReliableGlucose, let quantity = glucose.glucose else {
-//                return nil
-//            }
-//
-//            return NewGlucoseSample(
-//                date: glucose.readDate,
-//                quantity: quantity,
-//                trend: glucose.trendType,
-//                isDisplayOnly: glucose.isDisplayOnly,
-//                wasUserEntered: glucose.isDisplayOnly,
-//                syncIdentifier: glucose.syncIdentifier,
-//                device: device
-//            )
-//        }
+        let samples = glucose.filter { glucose -> Bool in
+            guard glucose != latestReading, glucose.state.hasReliableGlucose, glucose.glucose != nil else {
+                return false
+            }
+            return true
+        }
+        delegate?.transmitterManager(self, didRead: samples)
 //
 //        guard samples.count > 0 else {
 //            return

+ 3 - 3
Dependecies/CGMBLEKit/Common/HKUnit.swift

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

+ 10 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -8,7 +8,6 @@
 
 /* Begin PBXBuildFile section */
 		041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */; };
-		0AFC60BFFA3D9D0A80C807F4 /* CGMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0D51F68E37921622962DB4 /* CGMProvider.swift */; };
 		0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */; };
 		0D9A5E34A899219C5C4CDFAF /* DataTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableViewModel.swift */; };
 		17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CREditorProvider.swift */; };
@@ -118,6 +117,9 @@
 		385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */; };
 		385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC025F2EA52002D6D5B /* Announcement.swift */; };
 		385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */; };
+		386A124C271704DA00DDC61C /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; };
+		386A124D271704DA00DDC61C /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		386A124F271707F000DDC61C /* DexcomSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386A124E271707F000DDC61C /* DexcomSource.swift */; };
 		3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3870FF4225EC13F40088248F /* BloodGlucose.swift */; };
 		3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F38625ED661C0013ECB5 /* Suggestion.swift */; };
 		3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39B25ED892B0013ECB5 /* TempTarget.swift */; };
@@ -300,6 +302,7 @@
 				38887DE525F61F7500944304 /* LoopTestingKit.framework in Embed Frameworks */,
 				38887DE125F61F7500944304 /* LoopKit.framework in Embed Frameworks */,
 				38887DEF25F61F7500944304 /* MinimedKit.framework in Embed Frameworks */,
+				386A124D271704DA00DDC61C /* CGMBLEKit.framework in Embed Frameworks */,
 				38887DF325F61F7600944304 /* NightscoutUploadKit.framework in Embed Frameworks */,
 				38887DFB25F61F7600944304 /* RileyLinkKit.framework in Embed Frameworks */,
 				38887DFD25F61F7600944304 /* RileyLinkKitUI.framework in Embed Frameworks */,
@@ -439,6 +442,8 @@
 		385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStatus.swift; sourceTree = "<group>"; };
 		385CEAC025F2EA52002D6D5B /* Announcement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = "<group>"; };
 		385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsStorage.swift; sourceTree = "<group>"; };
+		386A124B271704DA00DDC61C /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		386A124E271707F000DDC61C /* DexcomSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSource.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
 		3871F38625ED661C0013ECB5 /* Suggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = "<group>"; };
 		3871F39B25ED892B0013ECB5 /* TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTarget.swift; sourceTree = "<group>"; };
@@ -610,6 +615,7 @@
 				38887DEC25F61F7500944304 /* Crypto.framework in Frameworks */,
 				38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */,
 				38887DF425F61F7600944304 /* OmniKit.framework in Frameworks */,
+				386A124C271704DA00DDC61C /* CGMBLEKit.framework in Frameworks */,
 				38887DFA25F61F7600944304 /* RileyLinkKit.framework in Frameworks */,
 				38887DF625F61F7600944304 /* OmniKitUI.framework in Frameworks */,
 				38887DE425F61F7500944304 /* LoopTestingKit.framework in Frameworks */,
@@ -1069,6 +1075,7 @@
 			children = (
 				38569346270B5DFB0002C50D /* AppGroupSource.swift */,
 				38569344270B5DFA0002C50D /* CGMType.swift */,
+				386A124E271707F000DDC61C /* DexcomSource.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
 			);
 			path = CGM;
@@ -1214,6 +1221,7 @@
 		38B17B8525DD93BA005CAE3D /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				386A124B271704DA00DDC61C /* CGMBLEKit.framework */,
 				38887E2025F6209B00944304 /* SwiftCharts.framework */,
 				38887DD025F61F7500944304 /* LoopKit.framework */,
 				38887DD125F61F7500944304 /* LoopKitUI.framework */,
@@ -1749,6 +1757,7 @@
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				38B4F3CF25E5041600E76A18 /* APSContainer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
+				386A124F271707F000DDC61C /* DexcomSource.swift in Sources */,
 				3811DE6E25C9D62600A708ED /* OnboardingViewModel.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				3811DE6D25C9D62600A708ED /* OnboardingRootView.swift in Sources */,
@@ -1956,7 +1965,6 @@
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
 				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
-				0AFC60BFFA3D9D0A80C807F4 /* CGMProvider.swift in Sources */,
 				BA00D96F7B2FF169A06FB530 /* CGMViewModel.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 14 - 0
FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS X.xcscheme

@@ -252,6 +252,20 @@
             buildForAnalyzing = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
+               BlueprintIdentifier = "43CABDF21C3506F100005705"
+               BuildableName = "CGMBLEKit.framework"
+               BlueprintName = "CGMBLEKit"
+               ReferencedContainer = "container:Dependecies/CGMBLEKit/CGMBLEKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
                BlueprintIdentifier = "43D5E78D1FAF7BFB004ACDB7"
                BuildableName = "RileyLinkKitUI.framework"
                BlueprintName = "RileyLinkKitUI"

+ 3 - 0
FreeAPS.xcworkspace/contents.xcworkspacedata

@@ -5,6 +5,9 @@
       location = "group:FreeAPS.xcodeproj">
    </FileRef>
    <FileRef
+      location = "group:Dependecies/CGMBLEKit/CGMBLEKit.xcodeproj">
+   </FileRef>
+   <FileRef
       location = "group:Dependecies/LoopKit/LoopKit.xcodeproj">
    </FileRef>
    <FileRef

+ 0 - 1
FreeAPS/Resources/Info.plist

@@ -29,7 +29,6 @@
 		<string>xdripswift</string>
 		<string>dexcomg6</string>
 		<string>dexcomcgm</string>
-		<string>dexcomshare</string>
 		<string>diabox</string>
 		<string>spikeapp</string>
 	</array>

+ 0 - 1
FreeAPS/Sources/APS/CGM/AppGroupSource.swift

@@ -33,7 +33,6 @@ struct AppGroupSource: GlucoseSource {
 
             results.append(
                 BloodGlucose(
-                    _id: UUID().uuidString,
                     sgv: glucose,
                     direction: BloodGlucose.Direction(rawValue: direction),
                     date: Decimal(Int(date.timeIntervalSince1970 * 1000)),

+ 18 - 2
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -5,7 +5,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
 
     case nightscout
     case xdrip
-//    case dexcom
+    case dexcomG6
+    case dexcomG5
 
     var displayName: String {
         switch self {
@@ -13,8 +14,23 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return "Nightscout"
         case .xdrip:
             return "xDrip"
+        case .dexcomG6:
+            return "Dexcom G6"
+        case .dexcomG5:
+            return "Dexcom G5"
         }
     }
 
-    static var allCases: [CGMType] = [.nightscout, .xdrip]
+    var appURL: URL? {
+        switch self {
+        case .nightscout:
+            return nil
+        case .xdrip:
+            return URL(string: "xdripswift://")!
+        case .dexcomG6:
+            return URL(string: "dexcomg6://")!
+        case .dexcomG5:
+            return URL(string: "dexcomgcgm://")!
+        }
+    }
 }

+ 79 - 0
FreeAPS/Sources/APS/CGM/DexcomSource.swift

@@ -0,0 +1,79 @@
+import CGMBLEKit
+import Combine
+import Foundation
+
+final class DexcomSource: GlucoseSource {
+    private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
+
+    @Persisted(key: "DexcomSource.transmitterID") var transmitterID: String? = nil
+
+    private let dexcomManager = TransmitterManager(state: TransmitterManagerState(transmitterID: "8MBPEY"))
+
+    private var promise: Future<[BloodGlucose], Error>.Promise?
+
+    init() {
+        dexcomManager.delegate = self
+    }
+
+    func fetch() -> AnyPublisher<[BloodGlucose], Never> {
+        dexcomManager.transmitter.resumeScanning()
+        return Future<[BloodGlucose], Error> { [weak self] promise in
+            self?.promise = promise
+        }
+        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
+        .replaceError(with: [])
+        .eraseToAnyPublisher()
+    }
+}
+
+extension DexcomSource: TransmitterManagerDelegate {
+    func transmitterManager(_: TransmitterManager, didFailWith error: Error) {
+        promise?(.failure(error))
+    }
+
+    func transmitterManager(_: TransmitterManager, didRead glucose: [CGMBLEKit.Glucose]) {
+        let bloodGlucose = glucose.compactMap { glucose -> BloodGlucose? in
+            guard let quantity = glucose.glucose else {
+                return nil
+            }
+            let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
+
+            return BloodGlucose(
+                sgv: value,
+                direction: .init(trend: glucose.trend),
+                date: Decimal(Int(glucose.readDate.timeIntervalSince1970 * 1000)),
+                dateString: glucose.readDate,
+                filtered: nil,
+                noise: nil,
+                glucose: value
+            )
+        }
+        promise?(.success(bloodGlucose))
+    }
+}
+
+extension BloodGlucose.Direction {
+    init(trend: Int) {
+        guard trend < Int(Int8.max) else {
+            self = .none
+            return
+        }
+
+        switch trend {
+        case let x where x <= -30:
+            self = .doubleDown
+        case let x where x <= -20:
+            self = .singleDown
+        case let x where x <= -10:
+            self = .fortyFiveDown
+        case let x where x < 10:
+            self = .flat
+        case let x where x < 20:
+            self = .fortyFiveUp
+        case let x where x < 30:
+            self = .singleUp
+        default:
+            self = .doubleUp
+        }
+    }
+}

+ 27 - 5
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -16,22 +16,33 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
 
     private lazy var appGroupSource = AppGroupSource()
+    private lazy var dexcomSource = DexcomSource()
 
     init(resolver: Resolver) {
         injectServices(resolver)
         subscribe()
     }
 
-    var glucoseSource: AnyPublisher<[BloodGlucose], Never> {
+    var glucoseSource: GlucoseSource {
         switch settingsManager.settings.cgm {
         case .xdrip:
-            return appGroupSource.fetch()
-        default:
-            return nightscoutManager.fetchGlucose()
+            return appGroupSource
+        case .dexcomG5,
+             .dexcomG6:
+            return dexcomSource
+        case .nightscout,
+             .none:
+            return nightscoutManager
         }
     }
 
     private func subscribe() {
+        UserDefaults.standard
+            .publisher(for: \.dexcomTransmitterID)
+            .sink { _ in
+            }
+            .store(in: &lifetime)
+
         timer.publisher
             .receive(on: processQueue)
             .flatMap { date -> AnyPublisher<(Date, Date, [BloodGlucose]), Never> in
@@ -40,7 +51,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                 return Publishers.CombineLatest3(
                     Just(date),
                     Just(self.glucoseStorage.syncDate()),
-                    self.glucoseSource
+                    self.glucoseSource.fetch()
                 )
                 .eraseToAnyPublisher()
             }
@@ -59,3 +70,14 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         timer.resume()
     }
 }
+
+extension UserDefaults {
+    @objc var dexcomTransmitterID: String? {
+        get {
+            string(forKey: "DexcomSource.transmitterID")
+        }
+        set {
+            set(newValue, forKey: "DexcomSource.transmitterID")
+        }
+    }
+}

+ 7 - 3
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -3,7 +3,7 @@ import Foundation
 import Swinject
 import UIKit
 
-protocol NightscoutManager {
+protocol NightscoutManager: GlucoseSource {
     func fetchGlucose() -> AnyPublisher<[BloodGlucose], Never>
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
     func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>
@@ -62,8 +62,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     var cgmURL: URL? {
-        if settingsManager.settings.cgm == .xdrip {
-            return URL(string: "xdripswift://")!
+        if let url = settingsManager.settings.cgm?.appURL {
+            return url
         }
 
         let useLocal = (settingsManager.settings.useLocalGlucoseSource ?? false) && settingsManager.settings
@@ -104,6 +104,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .eraseToAnyPublisher()
     }
 
+    func fetch() -> AnyPublisher<[BloodGlucose], Never> {
+        fetchGlucose()
+    }
+
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> {
         guard let nightscout = nightscoutAPI, isNetworkReachable else {
             return Just([]).eraseToAnyPublisher()