Просмотр исходного кода

Update the freeAPS source with new packages.

avouspierre 3 лет назад
Родитель
Сommit
6625b014ad

+ 1 - 1
Dependencies/CGMBLEKit/CGMBLEKit.xcodeproj/project.pbxproj

@@ -274,9 +274,9 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 43CABE071C3506F100005705 /* Build configuration list for PBXNativeTarget "CGMBLEKit" */;
 			buildPhases = (
+				43CABDF01C3506F100005705 /* Headers */,
 				43CABDEE1C3506F100005705 /* Sources */,
 				43CABDEF1C3506F100005705 /* Frameworks */,
-				43CABDF01C3506F100005705 /* Headers */,
 				43CABDF11C3506F100005705 /* Resources */,
 			);
 			buildRules = (

+ 24 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -286,6 +286,9 @@
 		CEB434DD28B8F5B900B70274 /* MKRingProgressView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CEB434DF28B8F5C400B70274 /* OmniBLE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DE28B8F5C400B70274 /* OmniBLE.framework */; };
 		CEB434E028B8F5C400B70274 /* OmniBLE.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DE28B8F5C400B70274 /* OmniBLE.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */; };
+		CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E428B8FF5D00B70274 /* UIColor.swift */; };
+		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
@@ -700,6 +703,9 @@
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MKRingProgressView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434DE28B8F5C400B70274 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = "<group>"; };
+		CEB434E428B8FF5D00B70274 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = "<group>"; };
+		CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopUIColorPalette+Default.swift"; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CREditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorRootView.swift; sourceTree = "<group>"; };
 		D295A3F870E826BE371C0BB5 /* AutotuneConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigStateModel.swift; sourceTree = "<group>"; };
@@ -990,6 +996,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				CEB434E128B8F9BC00B70274 /* Bluetooth */,
 				F90692A8274B7A980037068D /* HealthKit */,
 				38E8754D275556E100975559 /* WatchManager */,
 				38E87406274F9AA500975559 /* UserNotifiactions */,
@@ -1310,6 +1317,7 @@
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				E06B9119275B5EEA003C04B6 /* Array+Extension.swift */,
+				CEB434E428B8FF5D00B70274 /* UIColor.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -1331,6 +1339,7 @@
 			children = (
 				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
 				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
+				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
 			);
 			path = Extensions;
 			sourceTree = "<group>";
@@ -1720,6 +1729,14 @@
 			path = Bolus;
 			sourceTree = "<group>";
 		};
+		CEB434E128B8F9BC00B70274 /* Bluetooth */ = {
+			isa = PBXGroup;
+			children = (
+				CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */,
+			);
+			path = Bluetooth;
+			sourceTree = "<group>";
+		};
 		D533BF261CDC1C3F871E7BFD /* NightscoutConfig */ = {
 			isa = PBXGroup;
 			children = (
@@ -2128,6 +2145,7 @@
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
+				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
@@ -2161,6 +2179,7 @@
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
+				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
@@ -2170,6 +2189,7 @@
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
+				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
@@ -2485,7 +2505,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 14.3;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.5;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				ONLY_ACTIVE_ARCH = YES;
@@ -2543,7 +2563,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 14.3;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.5;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				SDKROOT = iphoneos;
@@ -2568,7 +2588,7 @@
 				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.5;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -2604,7 +2624,7 @@
 				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.5;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",

+ 2 - 0
FreeAPS/Resources/Info.plist

@@ -6,6 +6,8 @@
 	<string>$(APP_GROUP_ID)</string>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleDisplayName</key>
+	<string>$(APP_DISPLAY_NAME)</string>
 	<key>CFBundleExecutable</key>
 	<string>$(EXECUTABLE_NAME)</string>
 	<key>CFBundleIdentifier</key>

+ 32 - 30
FreeAPS/Sources/APS/APSManager.swift

@@ -2,6 +2,7 @@ import Combine
 import Foundation
 import LoopKit
 import LoopKitUI
+import RileyLinkKit
 import SwiftDate
 import Swinject
 
@@ -10,6 +11,7 @@ protocol APSManager {
     func autotune() -> AnyPublisher<Autotune?, Never>
     func enactBolus(amount: Double, isSMB: Bool)
     var pumpManager: PumpManagerUI? { get set }
+    var bluetoothManager: BluetoothStateManager? { get }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var isLooping: CurrentValueSubject<Bool, Never> { get }
@@ -78,6 +80,8 @@ final class BaseAPSManager: APSManager, Injectable {
         set { deviceDataManager.pumpManager = newValue }
     }
 
+    var bluetoothManager: BluetoothStateManager? { deviceDataManager.bluetoothManager }
+
     let isLooping = CurrentValueSubject<Bool, Never>(false)
     let lastLoopDateSubject = PassthroughSubject<Date, Never>()
     let lastError = CurrentValueSubject<Error?, Never>(nil)
@@ -386,18 +390,17 @@ final class BaseAPSManager: APSManager, Injectable {
         debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
 
         let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
-        pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { result in
-            switch result {
-            case .success:
+        pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { error in
+            if let error = error {
+                debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
+                self.processError(APSError.pumpError(error))
+            } else {
                 debug(.apsManager, "Temp Basal succeeded")
                 let temp = TempBasal(duration: Int(duration / 60), rate: Decimal(rate), temp: .absolute, timestamp: Date())
                 self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
                 if rate == 0, duration == 0 {
                     self.pumpHistoryStorage.saveCancelTempEvents()
                 }
-            case let .failure(error):
-                debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
-                self.processError(APSError.pumpError(error))
             }
         }
     }
@@ -441,14 +444,13 @@ final class BaseAPSManager: APSManager, Injectable {
                 return
             }
             let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
-            pump.enactBolus(units: roundedAmount, automatic: false) { result in
-                switch result {
-                case .success:
+            pump.enactBolus(units: roundedAmount, activationType: .manualRecommendationAccepted) { error in
+                if let error = error {
+                    warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
+                } else {
                     debug(.apsManager, "Announcement Bolus succeeded")
                     self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
                     self.bolusProgress.send(0)
-                case let .failure(error):
-                    warning(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
                 }
             }
         case let .pump(pumpAction):
@@ -494,13 +496,12 @@ final class BaseAPSManager: APSManager, Injectable {
                 return
             }
             let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
-            pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { result in
-                switch result {
-                case .success:
+            pump.enactTempBasal(unitsPerHour: roundedRate, for: TimeInterval(duration) * 60) { error in
+                if let error = error {
+                    warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
+                } else {
                     debug(.apsManager, "Announcement TempBasal succeeded")
                     self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
-                case let .failure(error):
-                    warning(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
                 }
             }
         }
@@ -617,16 +618,15 @@ final class BaseAPSManager: APSManager, Injectable {
 }
 
 private extension PumpManager {
-    func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry, Error> {
+    func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry?, Error> {
         Future { promise in
-            self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { result in
-                switch result {
-                case let .success(dose):
-                    debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
-                    promise(.success(dose))
-                case let .failure(error):
+            self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
+                if let error = error {
                     debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
                     promise(.failure(error))
+                } else {
+                    debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
+                    promise(.success(nil))
                 }
             }
         }
@@ -634,16 +634,18 @@ private extension PumpManager {
         .eraseToAnyPublisher()
     }
 
-    func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry, Error> {
+    func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry?, Error> {
         Future { promise in
-            self.enactBolus(units: units, automatic: automatic) { result in
-                switch result {
-                case let .success(dose):
-                    debug(.apsManager, "Bolus succeded: \(units)")
-                    promise(.success(dose))
-                case let .failure(error):
+            // convert automatic
+            let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
+
+            self.enactBolus(units: units, activationType: automaticValue) { error in
+                if let error = error {
                     debug(.apsManager, "Bolus failed: \(units)")
                     promise(.failure(error))
+                } else {
+                    debug(.apsManager, "Bolus succeded: \(units)")
+                    promise(.success(nil))
                 }
             }
         }

+ 71 - 9
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -5,6 +5,7 @@ import LoopKit
 import LoopKitUI
 import MinimedKit
 import MockKit
+import OmniBLE
 import OmniKit
 import SwiftDate
 import Swinject
@@ -12,6 +13,7 @@ import UserNotifications
 
 protocol DeviceDataManager: GlucoseSource {
     var pumpManager: PumpManagerUI? { get set }
+    var bluetoothManager: BluetoothStateManager { get }
     var loopInProgress: Bool { get set }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
     var recommendsLoop: PassthroughSubject<Void, Never> { get }
@@ -26,12 +28,20 @@ protocol DeviceDataManager: GlucoseSource {
 private let staticPumpManagers: [PumpManagerUI.Type] = [
     MinimedPumpManager.self,
     OmnipodPumpManager.self,
+    OmniBLEPumpManager.self,
     MockPumpManager.self
 ]
 
-private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = staticPumpManagers.reduce(into: [:]) { map, Type in
-    map[Type.managerIdentifier] = Type
-}
+private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
+    MinimedPumpManager.managerIdentifier: MinimedPumpManager.self,
+    OmnipodPumpManager.managerIdentifier: OmnipodPumpManager.self,
+    OmniBLEPumpManager.managerIdentifier: OmniBLEPumpManager.self,
+    MockPumpManager.managerIdentifier: MockPumpManager.self
+]
+
+// private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = staticPumpManagers.reduce(into: [:]) { map, Type in
+//    map[Type.managerIdentifier] = Type
+// }
 
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 
@@ -42,6 +52,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var bluetoothProvider: BluetoothStateManager!
 
     @Persisted(key: "BaseDeviceDataManager.lastEventDate") var lastEventDate: Date? = nil
     @SyncAccess(lock: accessLock) @Persisted(key: "BaseDeviceDataManager.lastHeartBeatTime") var lastHeartBeatTime: Date =
@@ -71,6 +82,13 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                     }
                     pumpExpiresAtDate.send(endTime)
                 }
+                if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
+                    guard let endTime = omnipodBLE.state.podState?.expiresAt else {
+                        pumpExpiresAtDate.send(nil)
+                        return
+                    }
+                    pumpExpiresAtDate.send(endTime)
+                }
             } else {
                 pumpDisplayState.value = nil
                 pumpExpiresAtDate.send(nil)
@@ -79,6 +97,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         }
     }
 
+    var bluetoothManager: BluetoothStateManager { bluetoothProvider }
+
     var hasBLEHeartbeat: Bool {
         (pumpManager as? MockPumpManager) == nil
     }
@@ -132,7 +152,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
             pumpUpdatePromise = promise
             debug(.deviceManager, "Waiting for pump update and loop recommendation")
             processQueue.safeSync {
-                pumpManager.ensureCurrentPumpData {
+                pumpManager.ensureCurrentPumpData { _ in
                     debug(.deviceManager, "Pump data updated.")
                 }
             }
@@ -197,6 +217,9 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                     case .noData:
                         debug(.deviceManager, "Minilink glucose is empty")
                         promise(.success([]))
+                    case .unreliableData:
+                        debug(.deviceManager, "Unreliable data received")
+                        promise(.success([]))
                     case let .newData(glucose):
                         let directions: [BloodGlucose.Direction?] = [nil]
                             + glucose.windows(ofCount: 2).map { window -> BloodGlucose.Direction? in
@@ -242,6 +265,15 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 }
 
 extension BaseDeviceDataManager: PumpManagerDelegate {
+    func pumpManagerPumpWasReplaced(_: PumpManager) {
+        debug(.deviceManager, "pumpManagerPumpWasReplaced")
+    }
+
+    var detectedSystemTimeOffset: TimeInterval {
+        // trustedTimeChecker.detectedSystemTimeOffset
+        0
+    }
+
     func pumpManager(_: PumpManager, didAdjustPumpClockBy adjustment: TimeInterval) {
         debug(.deviceManager, "didAdjustPumpClockBy \(adjustment)")
     }
@@ -299,6 +331,21 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             }
             pumpExpiresAtDate.send(endTime)
         }
+
+        if let omnipodBLE = pumpManager as? OmnipodPumpManager {
+            let reservoir = omnipodBLE.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
+
+            storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir)
+            broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
+                $0.pumpReservoirDidChange(Decimal(reservoir))
+            }
+
+            guard let endTime = omnipodBLE.state.podState?.expiresAt else {
+                pumpExpiresAtDate.send(nil)
+                return
+            }
+            pumpExpiresAtDate.send(endTime)
+        }
     }
 
     func pumpManagerWillDeactivate(_: PumpManager) {
@@ -317,7 +364,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManager(
         _: PumpManager,
         hasNewPumpEvents events: [NewPumpEvent],
-        lastReconciliation _: Date?,
+        lastSync _: Date?,
         completion: @escaping (_ error: Error?) -> Void
     ) {
         dispatchPrecondition(condition: .onQueue(processQueue))
@@ -375,6 +422,21 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 // MARK: - DeviceManagerDelegate
 
 extension BaseDeviceDataManager: DeviceManagerDelegate {
+    func issueAlert(_: Alert) {}
+
+    func retractAlert(identifier _: Alert.Identifier) {}
+
+    func doesIssuedAlertExist(identifier _: Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
+
+    func lookupAllUnretracted(managerIdentifier _: String, completion _: @escaping (Result<[PersistedAlert], Error>) -> Void) {}
+
+    func lookupAllUnacknowledgedUnretracted(
+        managerIdentifier _: String,
+        completion _: @escaping (Result<[PersistedAlert], Error>) -> Void
+    ) {}
+
+    func recordRetractedAlert(_: Alert, at _: Date) {}
+
     func scheduleNotification(
         for _: DeviceManager,
         identifier: String,
@@ -433,10 +495,10 @@ extension BaseDeviceDataManager: CGMManagerDelegate {
 
 // MARK: - AlertPresenter
 
-extension BaseDeviceDataManager: AlertPresenter {
-    func issueAlert(_: Alert) {}
-    func retractAlert(identifier _: Alert.Identifier) {}
-}
+// extension BaseDeviceDataManager: AlertPresenter {
+//    func issueAlert(_: Alert) {}
+//    func retractAlert(identifier _: Alert.Identifier) {}
+// }
 
 // MARK: Others
 

+ 51 - 0
FreeAPS/Sources/APS/Extensions/LoopUIColorPalette+Default.swift

@@ -0,0 +1,51 @@
+import LoopKitUI
+import SwiftUI
+
+extension StateColorPalette {
+    static let loopStatus = StateColorPalette(
+        unknown: .unknownColor,
+        normal: .freshColor,
+        warning: .agingColor,
+        error: .staleColor
+    )
+
+    static let cgmStatus = loopStatus
+
+    static let pumpStatus = StateColorPalette(
+        unknown: .unknownColor,
+        normal: .pumpStatusNormal,
+        warning: .agingColor,
+        error: .staleColor
+    )
+}
+
+extension ChartColorPalette {
+    static var primary: ChartColorPalette {
+        ChartColorPalette(
+            axisLine: .axisLineColor,
+            axisLabel: .axisLabelColor,
+            grid: .gridColor,
+            glucoseTint: .glucoseTintColor,
+            insulinTint: .insulinTintColor
+        )
+    }
+}
+
+public extension GuidanceColors {
+    static var `default`: GuidanceColors {
+        GuidanceColors(acceptable: .primary, warning: .warning, critical: .critical)
+    }
+}
+
+public extension LoopUIColorPalette {
+    static var `default`: LoopUIColorPalette {
+        LoopUIColorPalette(
+            guidanceColors: .default,
+            carbTintColor: .carbTintColor,
+            glucoseTintColor: .glucoseTintColor,
+            insulinTintColor: .insulinTintColor,
+            loopStatusColorPalette: .loopStatus,
+            chartColorPalette: .primary
+        )
+    }
+}

+ 21 - 12
FreeAPS/Sources/APS/Extensions/PumpManagerExtensions.swift

@@ -4,28 +4,37 @@ import LoopKitUI
 extension PumpManager {
     var rawValue: [String: Any] {
         [
-            "managerIdentifier": type(of: self).managerIdentifier,
+            "managerIdentifier": managerIdentifier, // "managerIdentifier": type(of: self).managerIdentifier,
             "state": rawState
         ]
     }
 }
 
 extension PumpManagerUI {
-    static func setupViewController() -> PumpManagerSetupViewController & UIViewController & CompletionNotifying {
-        setupViewController(
-            insulinTintColor: .accentColor,
-            guidanceColors: GuidanceColors(acceptable: .green, warning: .orange, critical: .red),
-            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
-        )
-    }
+//    static func setupViewController() -> PumpManagerSetupViewController & UIViewController & CompletionNotifying {
+//        setupViewController(
+//            insulinTintColor: .accentColor,
+//            guidanceColors: GuidanceColors(acceptable: .green, warning: .orange, critical: .red),
+//            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
+//        )
+//    }
 
-    func settingsViewController() -> UIViewController & CompletionNotifying {
+    func settingsViewController(bluetoothProvider: BluetoothProvider) -> UIViewController & CompletionNotifying {
         settingsViewController(
-            insulinTintColor: .accentColor,
-            guidanceColors: GuidanceColors(acceptable: .green, warning: .orange, critical: .red),
-            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
+            bluetoothProvider: bluetoothProvider,
+            colorPalette: .default,
+            allowDebugFeatures: false,
+            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
         )
     }
+
+//    func settingsViewController() -> UIViewController & CompletionNotifying {
+//        settingsViewController(
+//            insulinTintColor: .accentColor,
+//            guidanceColors: GuidanceColors(acceptable: .green, warning: .orange, critical: .red),
+//            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
+//        )
+//    }
 }
 
 protocol PumpSettingsBuilder {

+ 1 - 1
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -53,7 +53,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     let delivered = dose.deliveredUnits
                     let date = event.date
 
-                    let isCancel = !event.isMutable && delivered != nil
+                    let isCancel = delivered != nil //! event.isMutable && delivered != nil
                     guard !isCancel else { return [] }
 
                     return [

+ 1 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -39,6 +39,7 @@ import Swinject
         _ = resolver.resolve(UserNotificationsManager.self)!
         _ = resolver.resolve(WatchManager.self)!
         _ = resolver.resolve(HealthKitManager.self)!
+        _ = resolver.resolve(BluetoothStateManager.self)!
     }
 
     init() {

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

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

+ 48 - 1
FreeAPS/Sources/Helpers/Color+Extensions.swift

@@ -1,11 +1,58 @@
 import SwiftUI
+import UIKit
+
+extension Color {
+    static let carbs = Color("carbs")
+
+    static let fresh = Color("fresh")
+
+    static let glucose = Color("glucose")
+
+    static let insulin = Color("Insulin")
+
+    // The loopAccent color is intended to be use as the app accent color.
+    public static let loopAccent = Color("accent")
+
+    public static let warning = Color("warning")
+}
+
+// Color version of the UIColor context colors
+public extension Color {
+    static let agingColor = warning
+
+    static let axisLabelColor = secondary
+
+    static let axisLineColor = clear
+
+    #if os(iOS)
+        static let cellBackgroundColor = Color(UIColor.cellBackgroundColor)
+        static let gridColor = Color(UIColor.gridColor)
+        static let unknownColor = Color(UIColor.unknownColor)
+    #endif
+
+    static let carbTintColor = carbs
+
+    static let critical = red
+
+    static let destructive = critical
+
+    static let glucoseTintColor = glucose
+
+    static let invalid = critical
+
+    static let insulinTintColor = insulin
+
+    static let pumpStatusNormal = insulin
+
+    static let staleColor = critical
+}
 
 extension Color {
     static let loopGray = Color("LoopGray")
     static let loopGreen = Color("LoopGreen")
     static let loopYellow = Color("LoopYellow")
     static let loopRed = Color("LoopRed")
-    static let insulin = Color("Insulin")
+    //   static let insulin = Color("Insulin")
     static let uam = Color("UAM")
     static let zt = Color("ZT")
     static let tempBasal = Color("TempBasal")

+ 63 - 0
FreeAPS/Sources/Helpers/UIColor.swift

@@ -0,0 +1,63 @@
+import SwiftUI
+
+extension UIColor {
+    // MARK: - HIG colors
+
+    // See: https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/
+
+    // HIG Green has changed for iOS 13. This is the legacy color.
+    static func HIGGreenColor() -> UIColor {
+        UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1)
+    }
+}
+
+// MARK: - Color palette for common elements
+
+extension UIColor {
+    @nonobjc static let carbs = UIColor(named: "carbs") ?? systemGreen
+
+    @nonobjc static let fresh = UIColor(named: "fresh") ?? HIGGreenColor()
+
+    @nonobjc static let glucose = UIColor(named: "glucose") ?? systemTeal
+
+    @nonobjc static let insulin = UIColor(named: "insulin") ?? systemOrange
+
+    // The loopAccent color is intended to be use as the app accent color.
+    @nonobjc public static let loopAccent = UIColor(named: "accent") ?? systemBlue
+
+    @nonobjc public static let warning = UIColor(named: "warning") ?? systemYellow
+}
+
+// MARK: - Context for colors
+
+public extension UIColor {
+    @nonobjc static let agingColor = warning
+
+    @nonobjc static let axisLabelColor = secondaryLabel
+
+    @nonobjc static let axisLineColor = clear
+
+    @nonobjc static let cellBackgroundColor = secondarySystemBackground
+
+    @nonobjc static let carbTintColor = carbs
+
+    @nonobjc internal static let critical = systemRed
+
+    @nonobjc static let destructive = critical
+
+    @nonobjc static let freshColor = fresh
+
+    @nonobjc static let glucoseTintColor = glucose
+
+    @nonobjc static let gridColor = systemGray3
+
+    @nonobjc static let invalid = critical
+
+    @nonobjc static let insulinTintColor = insulin
+
+    @nonobjc static let pumpStatusNormal = insulin
+
+    @nonobjc static let staleColor = critical
+
+    @nonobjc static let unknownColor = systemGray4
+}

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

@@ -152,8 +152,14 @@ extension Home {
             $setupPump
                 .sink { [weak self] show in
                     guard let self = self else { return }
-                    if show, let pumpManager = self.provider.apsManager.pumpManager {
-                        let view = PumpConfig.PumpSettingsView(pumpManager: pumpManager, completionDelegate: self).asAny()
+                    if show, let pumpManager = self.provider.apsManager.pumpManager,
+                       let bluetoothProvider = self.provider.apsManager.bluetoothManager
+                    {
+                        let view = PumpConfig.PumpSettingsView(
+                            pumpManager: pumpManager,
+                            bluetoothManager: bluetoothProvider,
+                            completionDelegate: self
+                        ).asAny()
                         self.router.mainSecondaryModalView.send(view)
                     } else {
                         self.router.mainSecondaryModalView.send(nil)

+ 4 - 1
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift

@@ -173,7 +173,10 @@ extension PreferencesEditor {
                 Field(
                     displayName: "Bolus Increment",
                     type: .decimal(keypath: \.bolusIncrement),
-                    infoText: NSLocalizedString("Smallest SMB / SMB increment in oref0. Minimum amount for Medtronic pumps is 0.1 U, whereas for Omnipod it’s 0.05 U. The default value is 0.1.", comment: "Bolus Increment"),
+                    infoText: NSLocalizedString(
+                        "Smallest SMB / SMB increment in oref0. Minimum amount for Medtronic pumps is 0.1 U, whereas for Omnipod it’s 0.05 U. The default value is 0.1.",
+                        comment: "Bolus Increment"
+                    ),
                     settable: self
                 )
             ]

+ 1 - 0
FreeAPS/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift

@@ -8,6 +8,7 @@ enum PumpConfig {
     enum PumpType: Equatable {
         case minimed
         case omnipod
+        case omnipodBLE
         case simulator
     }
 

+ 11 - 2
FreeAPS/Sources/Modules/PumpConfig/PumpConfigStateModel.swift

@@ -44,9 +44,18 @@ extension PumpConfig.StateModel: CompletionDelegate {
     }
 }
 
-extension PumpConfig.StateModel: PumpManagerSetupViewControllerDelegate {
-    func pumpManagerSetupViewController(_: PumpManagerSetupViewController, didSetUpPumpManager pumpManager: PumpManagerUI) {
+extension PumpConfig.StateModel: PumpManagerOnboardingDelegate {
+    func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
         provider.setPumpManager(pumpManager)
         setupPump = false
     }
+
+    func pumpManagerOnboarding(didOnboardPumpManager _: PumpManagerUI) {
+        // nothing to do
+    }
+
+//    func pumpManagerSetupViewController(_: PumpManagerSetupViewController, didSetUpPumpManager pumpManager: PumpManagerUI) {
+//        provider.setPumpManager(pumpManager)
+//        setupPump = false
+//    }
 }

+ 35 - 27
FreeAPS/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -7,37 +7,45 @@ extension PumpConfig {
         @StateObject var state = StateModel()
 
         var body: some View {
-            Form {
-                Section(header: Text("Model")) {
-                    if let pumpState = state.pumpState {
-                        Button {
-                            state.setupPump = true
-                        } label: {
-                            HStack {
-                                Image(uiImage: pumpState.image ?? UIImage()).padding()
-                                Text(pumpState.name)
+            NavigationView {
+                Form {
+                    Section(header: Text("Model")) {
+                        if let pumpState = state.pumpState {
+                            Button {
+                                state.setupPump = true
+                            } label: {
+                                HStack {
+                                    Image(uiImage: pumpState.image ?? UIImage()).padding()
+                                    Text(pumpState.name)
+                                }
                             }
+                        } else {
+                            Button("Add Medtronic") { state.addPump(.minimed) }
+                            Button("Add Omnipod") { state.addPump(.omnipod) }
+                            Button("Add Omnipod Dash") { state.addPump(.omnipodBLE) }
+                            Button("Add Simulator") { state.addPump(.simulator) }
                         }
-                    } else {
-                        Button("Add Medtronic") { state.addPump(.minimed) }
-                        Button("Add Omnipod") { state.addPump(.omnipod) }
-                        Button("Add Simulator") { state.addPump(.simulator) }
                     }
                 }
-            }
-            .onAppear(perform: configureView)
-            .navigationTitle("Pump config")
-            .navigationBarTitleDisplayMode(.automatic)
-            .sheet(isPresented: $state.setupPump) {
-                if let pumpManager = state.provider.apsManager.pumpManager {
-                    PumpSettingsView(pumpManager: pumpManager, completionDelegate: state)
-                } else {
-                    PumpSetupView(
-                        pumpType: state.setupPumpType,
-                        pumpInitialSettings: state.initialSettings,
-                        completionDelegate: state,
-                        setupDelegate: state
-                    )
+                .onAppear(perform: configureView)
+                .navigationTitle("Pump config")
+                .navigationBarTitleDisplayMode(.automatic)
+                .sheet(isPresented: $state.setupPump) {
+                    if let pumpManager = state.provider.apsManager.pumpManager {
+                        PumpSettingsView(
+                            pumpManager: pumpManager,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            completionDelegate: state
+                        )
+                    } else {
+                        PumpSetupView(
+                            pumpType: state.setupPumpType,
+                            pumpInitialSettings: state.initialSettings,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            completionDelegate: state,
+                            setupDelegate: state
+                        )
+                    }
                 }
             }
         }

+ 2 - 1
FreeAPS/Sources/Modules/PumpConfig/View/PumpSettingsView.swift

@@ -5,10 +5,11 @@ import UIKit
 extension PumpConfig {
     struct PumpSettingsView: UIViewControllerRepresentable {
         let pumpManager: PumpManagerUI
+        let bluetoothManager: BluetoothStateManager
         weak var completionDelegate: CompletionDelegate?
 
         func makeUIViewController(context _: UIViewControllerRepresentableContext<PumpSettingsView>) -> UIViewController {
-            var vc = pumpManager.settingsViewController()
+            var vc = pumpManager.settingsViewController(bluetoothProvider: bluetoothManager)
             vc.completionDelegate = completionDelegate
             return vc
         }

+ 57 - 11
FreeAPS/Sources/Modules/PumpConfig/View/PumpSetupView.swift

@@ -4,6 +4,7 @@ import MinimedKit
 import MinimedKitUI
 import MockKit
 import MockKitUI
+import OmniBLE
 import OmniKit
 import OmniKitUI
 import SwiftUI
@@ -13,27 +14,72 @@ extension PumpConfig {
     struct PumpSetupView: UIViewControllerRepresentable {
         let pumpType: PumpType
         let pumpInitialSettings: PumpInitialSettings
+        let bluetoothManager: BluetoothStateManager
         weak var completionDelegate: CompletionDelegate?
-        weak var setupDelegate: PumpManagerSetupViewControllerDelegate?
+        weak var setupDelegate: PumpManagerOnboardingDelegate?
 
         func makeUIViewController(context _: UIViewControllerRepresentableContext<PumpSetupView>) -> UIViewController {
-            var setupViewController: PumpManagerSetupViewController & UIViewController & CompletionNotifying
+            // var setupViewController: PumpManagerSetupViewController & UIViewController & CompletionNotifying
+            var setupViewController: SetupUIResult<
+                PumpManagerViewController,
+                PumpManagerUI
+            >
+
+            let initialSettings = PumpManagerSetupSettings(
+                maxBasalRateUnitsPerHour: pumpInitialSettings.maxBasalRateUnitsPerHour,
+                maxBolusUnits: pumpInitialSettings.maxBolusUnits,
+                basalSchedule: pumpInitialSettings.basalSchedule
+            )
 
             switch pumpType {
             case .minimed:
-                setupViewController = MinimedPumpManager.setupViewController()
+                setupViewController = MinimedPumpManager.setupViewController(
+                    initialSettings: initialSettings,
+                    bluetoothProvider: bluetoothManager,
+                    colorPalette: .default,
+                    allowDebugFeatures: false,
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                )
             case .omnipod:
-                setupViewController = OmnipodPumpManager.setupViewController()
+                setupViewController = OmnipodPumpManager.setupViewController(
+                    initialSettings: initialSettings,
+                    bluetoothProvider: bluetoothManager,
+                    colorPalette: .default,
+                    allowDebugFeatures: false,
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                )
+            case .omnipodBLE:
+                setupViewController = OmniBLEPumpManager.setupViewController(
+                    initialSettings: initialSettings,
+                    bluetoothProvider: bluetoothManager,
+                    colorPalette: .default,
+                    allowDebugFeatures: false,
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                )
             case .simulator:
-                setupViewController = MockPumpManager.setupViewController()
+                setupViewController = MockPumpManager.setupViewController(
+                    initialSettings: initialSettings,
+                    bluetoothProvider: bluetoothManager,
+                    colorPalette: .default,
+                    allowDebugFeatures: false,
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                )
             }
 
-            setupViewController.setupDelegate = setupDelegate
-            setupViewController.completionDelegate = completionDelegate
-            setupViewController.maxBolusUnits = pumpInitialSettings.maxBolusUnits
-            setupViewController.maxBasalRateUnitsPerHour = pumpInitialSettings.maxBasalRateUnitsPerHour
-            setupViewController.basalSchedule = pumpInitialSettings.basalSchedule
-            return setupViewController
+            // setupViewController.setupDelegate = setupDelegate
+            // setupViewController.completionDelegate = completionDelegate
+            // return setupViewController
+
+            switch setupViewController {
+            case var .userInteractionRequired(setupViewControllerUI):
+                setupViewControllerUI.pumpManagerOnboardingDelegate = setupDelegate
+                setupViewControllerUI.completionDelegate = completionDelegate
+                return setupViewControllerUI
+            // show(setupViewController, sender: self)
+            case .createdAndOnboarded:
+                debug(.default, "Pump manager  created and onboarded")
+                return UIViewController() // TODO:
+            }
         }
 
         func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<PumpSetupView>) {}

+ 9 - 4
FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorProvider.swift

@@ -1,4 +1,5 @@
 import Combine
+import HealthKit
 import LoopKit
 import LoopKitUI
 
@@ -32,12 +33,16 @@ extension PumpSettingsEditor {
                 return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
             }
             // Don't ask why 🤦‍♂️
-            let sync = DeliveryLimitSettingsTableViewController(style: .grouped)
-            sync.maximumBasalRatePerHour = Double(settings.maxBasal)
-            sync.maximumBolus = Double(settings.maxBolus)
+            // let sync = DeliveryLimitSettingsTableViewController(style: .grouped)
+            let limits = DeliveryLimits(
+                maximumBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(settings.maxBasal)),
+                maximumBolus: HKQuantity(unit: .internationalUnit(), doubleValue: Double(settings.maxBolus))
+            )
+            // sync.maximumBasalRatePerHour = Double(settings.maxBasal)
+            // sync.maximumBolus = Double(settings.maxBolus)
             return Future { promise in
                 self.processQueue.async {
-                    pump.syncDeliveryLimitSettings(for: sync) { result in
+                    pump.syncDeliveryLimits(limits: limits) { result in
                         switch result {
                         case .success:
                             save()

+ 106 - 0
FreeAPS/Sources/Services/Bluetooth/BluetoothStateManager.swift

@@ -0,0 +1,106 @@
+import CoreBluetooth
+import LoopKit
+import LoopKitUI
+import Swinject
+
+protocol BluetoothStateManager: BluetoothProvider {}
+
+public class BaseBluetoothStateManager: NSObject, BluetoothStateManager, Injectable {
+    private var completion: ((BluetoothAuthorization) -> Void)?
+    private var centralManager: CBCentralManager?
+    private var bluetoothObservers = WeakSynchronizedSet<BluetoothObserver>()
+
+    init(resolver: Resolver) {
+        super.init()
+        injectServices(resolver)
+        if bluetoothAuthorization != .notDetermined {
+            centralManager = CBCentralManager(delegate: self, queue: nil)
+        }
+    }
+
+    public var bluetoothAuthorization: BluetoothAuthorization {
+        BluetoothAuthorization(CBCentralManager.authorization)
+    }
+
+    public var bluetoothState: BluetoothState {
+        guard let centralManager = centralManager else {
+            return .unknown
+        }
+        return BluetoothState(centralManager.state)
+    }
+
+    public func authorizeBluetooth(_ completion: @escaping (BluetoothAuthorization) -> Void) {
+        guard centralManager == nil else {
+            completion(bluetoothAuthorization)
+            return
+        }
+        self.completion = completion
+        centralManager = CBCentralManager(delegate: self, queue: nil)
+    }
+
+    public func addBluetoothObserver(_ observer: BluetoothObserver, queue: DispatchQueue = .main) {
+        bluetoothObservers.insert(observer, queue: queue)
+    }
+
+    public func removeBluetoothObserver(_ observer: BluetoothObserver) {
+        bluetoothObservers.removeElement(observer)
+    }
+}
+
+// MARK: - CBCentralManagerDelegate
+
+extension BaseBluetoothStateManager: CBCentralManagerDelegate {
+    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
+        if let completion = completion {
+            completion(bluetoothAuthorization)
+            self.completion = nil
+        }
+        bluetoothObservers.forEach { $0.bluetoothDidUpdateState(BluetoothState(central.state)) }
+    }
+}
+
+// MARK: - BluetoothAuthorization
+
+private extension BluetoothAuthorization {
+    init(_ authorization: CBManagerAuthorization) {
+        switch authorization {
+        case .notDetermined:
+            self = .notDetermined
+        case .restricted:
+            self = .restricted
+        case .denied:
+            self = .denied
+        case .allowedAlways:
+            self = .authorized
+        @unknown default:
+            self = .notDetermined
+        }
+    }
+}
+
+// MARK: - BluetoothState
+
+private extension BluetoothState {
+    init(_ state: CBManagerState) {
+        switch state {
+        case .unknown:
+            self = .unknown
+        case .resetting:
+            self = .resetting
+        case .unsupported:
+            #if IOS_SIMULATOR
+                self = .poweredOn // Simulator reports unsupported, but pretend it is powered on
+            #else
+                self = .unsupported
+            #endif
+        case .unauthorized:
+            self = .unauthorized
+        case .poweredOff:
+            self = .poweredOff
+        case .poweredOn:
+            self = .poweredOn
+        @unknown default:
+            self = .unknown
+        }
+    }
+}