Преглед на файлове

Remove all specific code for Dexcom and LibreTransmission
Use the PluginSource as a unique access to new CGM plugins published by Loop
Update all the code to store new settings
Update Nightscout to disable toggle button to upload glucose if pluginManager is used

Pierre L преди 2 години
родител
ревизия
47fa01786a
променени са 25 файла, в които са добавени 698 реда и са изтрити 782 реда
  1. 48 16
      FreeAPS.xcodeproj/project.pbxproj
  2. 1 0
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  3. 45 33
      FreeAPS/Sources/APS/CGM/CGMType.swift
  4. 0 191
      FreeAPS/Sources/APS/CGM/DexcomSourceG5.swift
  5. 0 202
      FreeAPS/Sources/APS/CGM/DexcomSourceG6.swift
  6. 0 1
      FreeAPS/Sources/APS/CGM/GlucoseSimulatorSource.swift
  7. 0 1
      FreeAPS/Sources/APS/CGM/GlucoseSource.swift
  8. 0 185
      FreeAPS/Sources/APS/CGM/LibreTransmitterSource.swift
  9. 27 32
      FreeAPS/Sources/APS/CGM/dexcomSourceG7.swift
  10. 0 1
      FreeAPS/Sources/APS/DeviceDataManager.swift
  11. 94 44
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  12. 277 0
      FreeAPS/Sources/APS/PluginManager.swift
  13. 1 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  14. 1 0
      FreeAPS/Sources/Assemblies/APSAssembly.swift
  15. 50 0
      FreeAPS/Sources/Helpers/PropertyWrappers/PersistedProperty.swift
  16. 6 1
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  17. 77 16
      FreeAPS/Sources/Modules/CGM/CGMStateModel.swift
  18. 14 9
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  19. 3 1
      FreeAPS/Sources/Modules/CGM/View/CGMSettingsView.swift
  20. 9 33
      FreeAPS/Sources/Modules/CGM/View/CGMSetupView.swift
  21. 3 13
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  22. 1 1
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  23. 0 1
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  24. 0 1
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  25. 41 0
      scripts/copy-plugins.sh

Файловите разлики са ограничени, защото са твърде много
+ 48 - 16
FreeAPS.xcodeproj/project.pbxproj


+ 1 - 0
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -12,6 +12,7 @@
   "skipBolusScreenAfterCarbs" : false,
   "displayHR" : false,
   "cgm" : "nightscout",
+  "cgmManagerTypeByIdentifier":"",
   "uploadGlucose" : true,
   "useCalendar" : false,
   "glucoseBadge" : false,

+ 45 - 33
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -2,59 +2,67 @@ import Foundation
 
 enum CGMType: String, JSON, CaseIterable, Identifiable {
     var id: String { rawValue }
-
+    case none
     case nightscout
     case xdrip
-    case dexcomG5
-    case dexcomG6
-    case dexcomG7
+//    case dexcomG5
+//    case dexcomG6
+//    case dexcomG7
     case simulator
-    case libreTransmitter
+//    case libreTransmitter
     case glucoseDirect
     case enlite
+    case plugin
 
     var displayName: String {
         switch self {
+        case .none:
+            return "None"
         case .nightscout:
             return "Nightscout"
         case .xdrip:
             return "xDrip4iOS"
         case .glucoseDirect:
             return "Glucose Direct"
-        case .dexcomG5:
-            return "Dexcom G5"
-        case .dexcomG6:
-            return "Dexcom G6"
-        case .dexcomG7:
-            return "Dexcom G7"
+//        case .dexcomG5:
+//            return "Dexcom G5"
+//        case .dexcomG6:
+//            return "Dexcom G6"
+//        case .dexcomG7:
+//            return "Dexcom G7"
         case .simulator:
             return NSLocalizedString("Glucose Simulator", comment: "Glucose Simulator CGM type")
-        case .libreTransmitter:
-            return NSLocalizedString("Libre Transmitter", comment: "Libre Transmitter type")
+//        case .libreTransmitter:
+//            return NSLocalizedString("Libre Transmitter", comment: "Libre Transmitter type")
         case .enlite:
             return "Medtronic Enlite"
+        case .plugin:
+            return "plugin CGM"
         }
     }
 
     var appURL: URL? {
         switch self {
         case .enlite,
-             .nightscout:
+             .nightscout,
+             .none:
             return nil
         case .xdrip:
             return URL(string: "xdripswift://")!
         case .glucoseDirect:
             return URL(string: "libredirect://")!
-        case .dexcomG5:
-            return URL(string: "dexcomgcgm://")!
-        case .dexcomG6:
-            return URL(string: "dexcomg6://")!
-        case .dexcomG7:
-            return URL(string: "dexcomg7://")!
+//        case .dexcomG5:
+//            return URL(string: "dexcomgcgm://")!
+//        case .dexcomG6:
+//            return URL(string: "dexcomg6://")!
+//        case .dexcomG7:
+//            return URL(string: "dexcomg7://")!
         case .simulator:
             return nil
-        case .libreTransmitter:
-            return URL(string: "freeaps-x://libre-transmitter")!
+//        case .libreTransmitter:
+//            return URL(string: "freeaps-x://libre-transmitter")!
+        case .plugin:
+            return nil
         }
     }
 
@@ -70,6 +78,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
 
     var subtitle: String {
         switch self {
+        case .none:
+            return NSLocalizedString("None", comment: "No CGM choiced")
         case .nightscout:
             return NSLocalizedString("Online or internal server", comment: "Online or internal server")
         case .xdrip:
@@ -77,19 +87,19 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
                 "Using shared app group with external CGM app xDrip4iOS",
                 comment: "Shared app group xDrip4iOS"
             )
-        case .dexcomG5:
-            return NSLocalizedString("Native G5 app", comment: "Native G5 app")
-        case .dexcomG6:
-            return NSLocalizedString("Dexcom G6 app", comment: "Dexcom G6 app")
-        case .dexcomG7:
-            return NSLocalizedString("Dexcom G7 app", comment: "Dexcom G76 app")
+//        case .dexcomG5:
+//            return NSLocalizedString("Native G5 app", comment: "Native G5 app")
+//        case .dexcomG6:
+//            return NSLocalizedString("Dexcom G6 app", comment: "Dexcom G6 app")
+//        case .dexcomG7:
+//            return NSLocalizedString("Dexcom G7 app", comment: "Dexcom G76 app")
         case .simulator:
             return NSLocalizedString("Simple simulator", comment: "Simple simulator")
-        case .libreTransmitter:
-            return NSLocalizedString(
-                "Direct connection with Libre 1 transmitters or European Libre 2 sensors",
-                comment: "Direct connection with Libre 1 transmitters or European Libre 2 sensors"
-            )
+//        case .libreTransmitter:
+//            return NSLocalizedString(
+//                "Direct connection with Libre 1 transmitters or European Libre 2 sensors",
+//                comment: "Direct connection with Libre 1 transmitters or European Libre 2 sensors"
+//            )
         case .glucoseDirect:
             return NSLocalizedString(
                 "Using shared app group with external CGM app GlucoseDirect",
@@ -97,6 +107,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             )
         case .enlite:
             return NSLocalizedString("Minilink transmitter", comment: "Minilink transmitter")
+        case .plugin:
+            return NSLocalizedString("Plugin CGM", comment: "Plugin CGM")
         }
     }
 }

+ 0 - 191
FreeAPS/Sources/APS/CGM/DexcomSourceG5.swift

@@ -1,191 +0,0 @@
-import CGMBLEKit
-import Combine
-import Foundation
-import LoopKit
-import LoopKitUI
-import ShareClient
-
-final class DexcomSourceG5: GlucoseSource {
-    private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
-    private let glucoseStorage: GlucoseStorage!
-    var glucoseManager: FetchGlucoseManager?
-
-    var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .dexcomG5
-
-    var cgmHasValidSensorSession: Bool = false
-
-    private var promise: Future<[BloodGlucose], Error>.Promise?
-
-    init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
-        self.glucoseStorage = glucoseStorage
-        self.glucoseManager = glucoseManager
-        cgmManager = G5CGMManager
-            .init(state: TransmitterManagerState(
-                transmitterID: UserDefaults.standard
-                    .dexcomTransmitterID ?? "000000",
-                shouldSyncToRemoteService: glucoseManager.settingsManager.settings.uploadGlucose
-            ))
-        cgmManager?.cgmManagerDelegate = self
-    }
-
-    var transmitterID: String {
-        guard let cgmG5Manager = cgmManager as? G5CGMManager else { return "000000" }
-        return cgmG5Manager.transmitter.ID
-    }
-
-    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
-        }
-        .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { _ in
-            self.processQueue.async {
-                guard let cgmManager = self.cgmManager else { return }
-                cgmManager.fetchNewDataIfNeeded { result in
-                    self.processCGMReadingResult(cgmManager, readingResult: result) {
-                        // nothing to do
-                    }
-                }
-            }
-        }
-        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    deinit {
-        // dexcomManager.transmitter.stopScanning()
-    }
-}
-
-extension DexcomSourceG5: CGMManagerDelegate {
-    func deviceManager(
-        _: LoopKit.DeviceManager,
-        logEventForDeviceIdentifier deviceIdentifier: String?,
-        type _: LoopKit.DeviceLogEntryType,
-        message: String,
-        completion _: ((Error?) -> Void)?
-    ) {
-        debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
-    }
-
-    func issueAlert(_: LoopKit.Alert) {}
-
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
-
-    func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
-
-    func lookupAllUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func lookupAllUnacknowledgedUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
-
-    func cgmManagerWantsDeletion(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(.main))
-        debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
-        glucoseManager?.cgmGlucoseSourceType = nil
-    }
-
-    func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
-        dispatchPrecondition(condition: .onQueue(.main))
-        processCGMReadingResult(manager, readingResult: readingResult) {
-            debug(.deviceManager, "DEXCOM - Direct return done")
-        }
-    }
-
-    func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        // TODO: Events in APS ?
-        // currently only display in log the date of the event
-        events.forEach { debug(.deviceManager, "events from CGM at \($0.date)") }
-    }
-
-    func startDateToFilterNewData(for _: CGMManager) -> Date? {
-        dispatchPrecondition(condition: .onQueue(.main))
-        return glucoseStorage.lastGlucoseDate()
-        //  return glucoseStore.latestGlucose?.startDate
-    }
-
-    func cgmManagerDidUpdateState(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        guard let g5Manager = manager as? TransmitterManager else {
-            return
-        }
-        glucoseManager?.settingsManager.settings.uploadGlucose = g5Manager.shouldSyncToRemoteService
-        UserDefaults.standard.dexcomTransmitterID = g5Manager.rawState["transmitterID"] as? String
-    }
-
-    func credentialStoragePrefix(for _: CGMManager) -> String {
-        // return string unique to this instance of the CGMManager
-        UUID().uuidString
-    }
-
-    func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
-        DispatchQueue.main.async {
-            if self.cgmHasValidSensorSession != status.hasValidSensorSession {
-                self.cgmHasValidSensorSession = status.hasValidSensorSession
-            }
-        }
-    }
-
-    private func processCGMReadingResult(
-        _: CGMManager,
-        readingResult: CGMReadingResult,
-        completion: @escaping () -> Void
-    ) {
-        debug(.deviceManager, "DEXCOM - Process CGM Reading Result launched")
-        switch readingResult {
-        case let .newData(values):
-            let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
-                let quantity = newGlucoseSample.quantity
-                let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
-                return BloodGlucose(
-                    _id: UUID().uuidString,
-                    sgv: value,
-                    direction: .init(trendType: newGlucoseSample.trend),
-                    date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
-                    dateString: newGlucoseSample.date,
-                    unfiltered: Decimal(value),
-                    filtered: nil,
-                    noise: nil,
-                    glucose: value,
-                    type: "sgv",
-                    transmitterID: self.transmitterID
-                )
-            }
-            promise?(.success(bloodGlucose))
-            completion()
-        case .unreliableData:
-            // loopManager.receivedUnreliableCGMReading()
-            promise?(.failure(GlucoseDataError.unreliableData))
-            completion()
-        case .noData:
-            promise?(.failure(GlucoseDataError.noData))
-            completion()
-        case let .error(error):
-            promise?(.failure(error))
-            completion()
-        }
-    }
-}
-
-extension DexcomSourceG5 {
-    func sourceInfo() -> [String: Any]? {
-        [GlucoseSourceKey.description.rawValue: "Dexcom tramsmitter ID: \(transmitterID)"]
-    }
-}

+ 0 - 202
FreeAPS/Sources/APS/CGM/DexcomSourceG6.swift

@@ -1,202 +0,0 @@
-import CGMBLEKit
-import Combine
-import Foundation
-import LoopKit
-import LoopKitUI
-import ShareClient
-
-final class DexcomSourceG6: GlucoseSource {
-    private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
-    private let glucoseStorage: GlucoseStorage!
-    var glucoseManager: FetchGlucoseManager?
-
-    var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .dexcomG6
-
-    var cgmHasValidSensorSession: Bool = false
-
-    private var promise: Future<[BloodGlucose], Error>.Promise?
-
-    init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
-        self.glucoseStorage = glucoseStorage
-        self.glucoseManager = glucoseManager
-        cgmManager = G6CGMManager
-            .init(state: TransmitterManagerState(
-                transmitterID: UserDefaults.standard
-                    .dexcomTransmitterID ?? "000000",
-                shouldSyncToRemoteService: glucoseManager.settingsManager.settings.uploadGlucose
-            ))
-        cgmManager?.delegateQueue = processQueue
-        cgmManager?.cgmManagerDelegate = self
-    }
-
-    var transmitterID: String {
-        guard let cgmG6Manager = cgmManager as? G6CGMManager else { return "000000" }
-        return cgmG6Manager.transmitter.ID
-    }
-
-    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
-        }
-        .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { _ in
-            self.processQueue.async {
-                guard let cgmManager = self.cgmManager else { return }
-                cgmManager.fetchNewDataIfNeeded { result in
-                    self.processCGMReadingResult(cgmManager, readingResult: result) {
-                        // nothing to do
-                    }
-                }
-            }
-        }
-        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    deinit {
-        // dexcomManager.transmitter.stopScanning()
-    }
-}
-
-extension DexcomSourceG6: CGMManagerDelegate {
-    func deviceManager(
-        _: LoopKit.DeviceManager,
-        logEventForDeviceIdentifier deviceIdentifier: String?,
-        type _: LoopKit.DeviceLogEntryType,
-        message: String,
-        completion _: ((Error?) -> Void)?
-    ) {
-        debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
-    }
-
-    func issueAlert(_: LoopKit.Alert) {}
-
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
-
-    func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
-
-    func lookupAllUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func lookupAllUnacknowledgedUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
-
-    func cgmManagerWantsDeletion(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
-        glucoseManager?.cgmGlucoseSourceType = nil
-    }
-
-    func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        processCGMReadingResult(manager, readingResult: readingResult) {
-            debug(.deviceManager, "DEXCOM - Direct return done")
-        }
-    }
-
-    func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        // TODO: Events in APS ?
-        // currently only display in log the date of the event
-        events.forEach { debug(.deviceManager, "events from CGM at \($0.date)") }
-    }
-
-    func startDateToFilterNewData(for _: CGMManager) -> Date? {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        return glucoseStorage.lastGlucoseDate()
-        //  return glucoseStore.latestGlucose?.startDate
-    }
-
-    func cgmManagerDidUpdateState(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        guard let g6Manager = manager as? TransmitterManager else {
-            return
-        }
-        glucoseManager?.settingsManager.settings.uploadGlucose = g6Manager.shouldSyncToRemoteService
-        UserDefaults.standard.dexcomTransmitterID = g6Manager.rawState["transmitterID"] as? String
-    }
-
-    func credentialStoragePrefix(for _: CGMManager) -> String {
-        // return string unique to this instance of the CGMManager
-        UUID().uuidString
-    }
-
-    func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
-        processQueue.async {
-            if self.cgmHasValidSensorSession != status.hasValidSensorSession {
-                self.cgmHasValidSensorSession = status.hasValidSensorSession
-            }
-        }
-    }
-
-    private func processCGMReadingResult(
-        _: CGMManager,
-        readingResult: CGMReadingResult,
-        completion: @escaping () -> Void
-    ) {
-        debug(.deviceManager, "DEXCOM - Process CGM Reading Result launched with \(readingResult)")
-        switch readingResult {
-        case let .newData(values):
-            if let cgmG6Manager = cgmManager as? G6CGMManager,
-               let activationDate = cgmG6Manager.latestReading?.activationDate,
-               let sessionStartDate = cgmG6Manager.latestReading?.sessionStartDate
-            {
-                let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
-                    let quantity = newGlucoseSample.quantity
-                    let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
-                    return BloodGlucose(
-                        _id: UUID().uuidString,
-                        sgv: value,
-                        direction: .init(trendType: newGlucoseSample.trend),
-                        date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
-                        dateString: newGlucoseSample.date,
-                        unfiltered: Decimal(value),
-                        filtered: nil,
-                        noise: nil,
-                        glucose: value,
-                        type: "sgv",
-                        activationDate: activationDate,
-                        sessionStartDate: sessionStartDate,
-                        transmitterID: self.transmitterID
-                    )
-                }
-                promise?(.success(bloodGlucose))
-                completion()
-            } else {
-                // Handle the case where activationDate or sessionStartDate is nil
-                completion()
-            }
-        case .unreliableData:
-            // loopManager.receivedUnreliableCGMReading()
-            promise?(.failure(GlucoseDataError.unreliableData))
-            completion()
-        case .noData:
-            promise?(.failure(GlucoseDataError.noData))
-            completion()
-        case let .error(error):
-            promise?(.failure(error))
-            completion()
-        }
-    }
-}
-
-extension DexcomSourceG6 {
-    func sourceInfo() -> [String: Any]? {
-        [GlucoseSourceKey.description.rawValue: "Dexcom tramsmitter ID: \(transmitterID)"]
-    }
-}

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

@@ -29,7 +29,6 @@ import LoopKitUI
 final class GlucoseSimulatorSource: GlucoseSource {
     var cgmManager: CGMManagerUI?
     var glucoseManager: FetchGlucoseManager?
-    var cgmType: CGMType = .simulator
 
     private enum Config {
         // min time period to publish data

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

@@ -11,7 +11,6 @@ protocol GlucoseSource: SourceInfoProvider {
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never>
     var glucoseManager: FetchGlucoseManager? { get set }
     var cgmManager: CGMManagerUI? { get set }
-    var cgmType: CGMType { get }
 }
 
 extension GlucoseSource {

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

@@ -1,185 +0,0 @@
-import Combine
-import Foundation
-import LibreTransmitter
-import LibreTransmitterUI
-import LoopKit
-import LoopKitUI
-import Swinject
-
-// protocol LibreTransmitterSource: GlucoseSource {
-//    var manager: LibreTransmitterManager? { get set }
-// }
-
-final class LibreTransmitterSource: GlucoseSource {
-    private let processQueue = DispatchQueue(label: "BaseLibreTransmitterSource.processQueue")
-    private var glucoseStorage: GlucoseStorage!
-    var glucoseManager: FetchGlucoseManager?
-
-    var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .libreTransmitter
-
-    var cgmHasValidSensorSession: Bool = false
-
-    // @Injected() var glucoseStorage: GlucoseStorage!
-//    @Injected() var calibrationService: CalibrationService!
-
-    private var promise: Future<[BloodGlucose], Error>.Promise?
-
-//    @Persisted(key: "LibreTransmitterManager.configured") private(set) var configured = false
-
-    init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
-        self.glucoseStorage = glucoseStorage
-        self.glucoseManager = glucoseManager
-        cgmManager = LibreTransmitterManagerV3()
-        cgmManager?.cgmManagerDelegate = self
-        cgmManager?.delegateQueue = processQueue
-    }
-
-    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
-        }
-        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { _ in
-            self.processQueue.async {
-                guard let cgmManager = self.cgmManager else { return }
-                cgmManager.fetchNewDataIfNeeded { result in
-                    self.processCGMReadingResult(cgmManager, readingResult: result) {
-                        // nothing to do
-                    }
-                }
-            }
-        }
-        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-//    func sourceInfo() -> [String: Any]? {
-//        if let battery = manager?.battery {
-//            return ["transmitterBattery": battery]
-//        }
-//        return nil
-//    }
-}
-
-extension LibreTransmitterSource: CGMManagerDelegate {
-    private func processCGMReadingResult(
-        _: CGMManager,
-        readingResult: CGMReadingResult,
-        completion: @escaping () -> Void
-    ) {
-        switch readingResult {
-        case let .newData(values):
-            if let libreManager = cgmManager as? LibreTransmitterManagerV3 {
-                let glucose = values.compactMap { newGlucoseSample -> BloodGlucose in
-                    let quantity = newGlucoseSample.quantity
-                    let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
-                    return BloodGlucose(
-                        _id: UUID().uuidString,
-                        sgv: value,
-                        direction: .init(trendType: newGlucoseSample.trend),
-                        date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
-                        dateString: newGlucoseSample.date,
-                        unfiltered: Decimal(value),
-                        filtered: nil,
-                        noise: nil,
-                        glucose: value,
-                        type: "sgv",
-                        activationDate: libreManager.sensorInfoObservable.activatedAt,
-                        sessionStartDate: libreManager.sensorInfoObservable.activatedAt,
-                        transmitterID: libreManager.sensorInfoObservable.sensorSerial
-                    )
-                }
-                NSLog("Debug Libre \(glucose)")
-                promise?(.success(glucose))
-                completion()
-            }
-
-        case .unreliableData:
-            promise?(.failure(GlucoseDataError.unreliableData))
-            completion()
-        case .noData:
-            promise?(.failure(GlucoseDataError.noData))
-            completion()
-        case let .error(error):
-            promise?(.failure(error))
-            completion()
-        }
-    }
-
-    func cgmManager(_ manager: LoopKit.CGMManager, hasNew readingResult: LoopKit.CGMReadingResult) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        processCGMReadingResult(manager, readingResult: readingResult) {
-            debug(.deviceManager, "Libre transmitter - Direct return done")
-        }
-    }
-
-    func cgmManagerDidUpdateState(_: LoopKit.CGMManager) {
-        // TODO: if useful in regard of configuration
-    }
-
-    func cgmManager(_: LoopKit.CGMManager, didUpdate status: LoopKit.CGMManagerStatus) {
-        DispatchQueue.main.async {
-            if self.cgmHasValidSensorSession != status.hasValidSensorSession {
-                self.cgmHasValidSensorSession = status.hasValidSensorSession
-            }
-        }
-    }
-
-    func startDateToFilterNewData(for _: LoopKit.CGMManager) -> Date? {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        return glucoseStorage.lastGlucoseDate()
-    }
-
-    func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
-        // TODO: Events in APS ?
-        // currently only display in log the date of the event
-        events.forEach { debug(.deviceManager, "events from CGM at \($0.date)") }
-    }
-
-    func cgmManagerWantsDeletion(_ manager: LoopKit.CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
-        glucoseManager?.cgmGlucoseSourceType = nil
-    }
-
-    func credentialStoragePrefix(for _: LoopKit.CGMManager) -> String {
-        UUID().uuidString
-    }
-
-    func deviceManager(
-        _: LoopKit.DeviceManager,
-        logEventForDeviceIdentifier deviceIdentifier: String?,
-        type _: LoopKit.DeviceLogEntryType,
-        message: String,
-        completion _: ((Error?) -> Void)?
-    ) {
-        debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
-    }
-
-    func issueAlert(_: LoopKit.Alert) {}
-
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
-
-    func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
-
-    func lookupAllUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func lookupAllUnacknowledgedUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
-}

+ 27 - 32
FreeAPS/Sources/APS/CGM/dexcomSourceG7.swift

@@ -1,17 +1,15 @@
-
 import Combine
 import Foundation
-import G7SensorKit
 import LoopKit
 import LoopKitUI
 
-final class DexcomSourceG7: GlucoseSource {
+final class PluginSource: GlucoseSource {
     private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
-    private var glucoseStorage: GlucoseStorage!
+    private let glucoseStorage: GlucoseStorage!
     var glucoseManager: FetchGlucoseManager?
 
     var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .dexcomG7
+
     var cgmHasValidSensorSession: Bool = false
 
     private var promise: Future<[BloodGlucose], Error>.Promise?
@@ -19,14 +17,10 @@ final class DexcomSourceG7: GlucoseSource {
     init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
         self.glucoseStorage = glucoseStorage
         self.glucoseManager = glucoseManager
-        cgmManager = G7CGMManager()
-        cgmManager?.cgmManagerDelegate = self
-        cgmManager?.delegateQueue = processQueue
 
-        // initial value of upload Readings
-        if let cgmManagerG7 = cgmManager as? G7CGMManager {
-            cgmManagerG7.uploadReadings = glucoseManager.settingsManager.settings.uploadGlucose
-        }
+        cgmManager = glucoseManager.cgmManager
+        cgmManager?.delegateQueue = processQueue
+        cgmManager?.cgmManagerDelegate = self
     }
 
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
@@ -61,7 +55,7 @@ final class DexcomSourceG7: GlucoseSource {
     }
 }
 
-extension DexcomSourceG7: CGMManagerDelegate {
+extension PluginSource: CGMManagerDelegate {
     func deviceManager(
         _: LoopKit.DeviceManager,
         logEventForDeviceIdentifier deviceIdentifier: String?,
@@ -93,13 +87,14 @@ extension DexcomSourceG7: CGMManagerDelegate {
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
+        // TODO:
         glucoseManager?.cgmGlucoseSourceType = nil
     }
 
     func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         processCGMReadingResult(manager, readingResult: readingResult) {
-            debug(.deviceManager, "DEXCOM - Direct return done")
+            debug(.deviceManager, "CGM PLUGIN - Direct return done")
         }
     }
 
@@ -115,10 +110,13 @@ extension DexcomSourceG7: CGMManagerDelegate {
         return glucoseStorage.lastGlucoseDate()
     }
 
-    func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {
-        if let cgmManagerG7 = cgmManager as? G7CGMManager {
-            glucoseManager?.settingsManager.settings.uploadGlucose = cgmManagerG7.uploadReadings
-        }
+    func cgmManagerDidUpdateState(_: CGMManager) {
+        dispatchPrecondition(condition: .onQueue(processQueue))
+//        guard let g6Manager = manager as? TransmitterManager else {
+//            return
+//        }
+//        glucoseManager?.settingsManager.settings.uploadGlucose = g6Manager.shouldSyncToRemoteService
+//        UserDefaults.standard.dexcomTransmitterID = g6Manager.rawState["transmitterID"] as? String
     }
 
     func credentialStoragePrefix(for _: CGMManager) -> String {
@@ -139,18 +137,10 @@ extension DexcomSourceG7: CGMManagerDelegate {
         readingResult: CGMReadingResult,
         completion: @escaping () -> Void
     ) {
-        debug(.deviceManager, "DEXCOMG7 - Process CGM Reading Result launched")
+        debug(.deviceManager, "PLUGIN CGM - Process CGM Reading Result launched with \(readingResult)")
         switch readingResult {
         case let .newData(values):
 
-            var activationDate: Date = .distantPast
-            var sessionStart: Date = .distantPast
-            if let cgmG7Manager = cgmManager as? G7CGMManager {
-                activationDate = cgmG7Manager.sensorActivatedAt ?? .distantPast
-                sessionStart = cgmG7Manager.sensorFinishesWarmupAt ?? .distantPast
-                print("Activastion date: " + activationDate.description)
-            }
-
             let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
                 let quantity = newGlucoseSample.quantity
                 let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
@@ -164,14 +154,13 @@ extension DexcomSourceG7: CGMManagerDelegate {
                     filtered: nil,
                     noise: nil,
                     glucose: value,
-                    type: "sgv",
-                    activationDate: activationDate,
-                    sessionStartDate: sessionStart
+                    type: "sgv"
+//                    activationDate: activationDate,
+//                    sessionStartDate: sessionStartDate
+//                    transmitterID: self.transmitterID
                 )
             }
-
             promise?(.success(bloodGlucose))
-
             completion()
         case .unreliableData:
             // loopManager.receivedUnreliableCGMReading()
@@ -186,3 +175,9 @@ extension DexcomSourceG7: CGMManagerDelegate {
         }
     }
 }
+
+extension PluginSource {
+    func sourceInfo() -> [String: Any]? {
+        [GlucoseSourceKey.description.rawValue: "Plugin CGM source"]
+    }
+}

+ 0 - 1
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -218,7 +218,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 
     var glucoseManager: FetchGlucoseManager?
     var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .enlite
 
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
         fetch(nil)

+ 94 - 44
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -1,5 +1,7 @@
 import Combine
 import Foundation
+import LoopKit
+import LoopKitUI
 import SwiftDate
 import Swinject
 import UIKit
@@ -7,10 +9,20 @@ import UIKit
 protocol FetchGlucoseManager: SourceInfoProvider {
     func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
     func refreshCGM()
-    func updateGlucoseSource()
+    func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
+    func deleteGlucoseSource()
     var glucoseSource: GlucoseSource! { get }
+    var cgmManager: CGMManagerUI? { get }
     var cgmGlucoseSourceType: CGMType? { get set }
+    var cgmGlucosePluginId: String? { get }
     var settingsManager: SettingsManager! { get }
+    var shouldSyncToRemoteService: Bool { get }
+}
+
+extension FetchGlucoseManager {
+    func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String) {
+        updateGlucoseSource(cgmGlucoseSourceType: cgmGlucoseSourceType, cgmGlucosePluginId: cgmGlucosePluginId, newManager: nil)
+    }
 }
 
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
@@ -21,48 +33,97 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @Injected() var settingsManager: SettingsManager!
     @Injected() var healthKitManager: HealthKitManager!
     @Injected() var deviceDataManager: DeviceDataManager!
+    @Injected() var pluginCGMManager: PluginManager!
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
     var cgmGlucoseSourceType: CGMType?
+    var cgmGlucosePluginId: String?
+    var cgmManager: CGMManagerUI? {
+        didSet {
+            rawCGMManager = cgmManager?.rawValue
+        }
+    }
+
+    @PersistedProperty(key: "CGMManagerState") var rawCGMManager: CGMManager.RawValue?
 
-    private lazy var dexcomSourceG5 = DexcomSourceG5(glucoseStorage: glucoseStorage, glucoseManager: self)
-    private lazy var dexcomSourceG6 = DexcomSourceG6(glucoseStorage: glucoseStorage, glucoseManager: self)
-    private lazy var dexcomSourceG7 = DexcomSourceG7(glucoseStorage: glucoseStorage, glucoseManager: self)
-    private lazy var libreSource = LibreTransmitterSource(glucoseStorage: glucoseStorage, glucoseManager: self)
     private lazy var simulatorSource = GlucoseSimulatorSource()
 
+    var shouldSyncToRemoteService: Bool {
+        guard let cgmManager = cgmManager else {
+            return true
+        }
+        return cgmManager.shouldSyncToRemoteService
+    }
+
     init(resolver: Resolver) {
         injectServices(resolver)
-        updateGlucoseSource()
+        updateGlucoseSource(
+            cgmGlucoseSourceType: settingsManager.settings.cgm,
+            cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
+        )
         subscribe()
     }
 
     var glucoseSource: GlucoseSource!
 
-    func updateGlucoseSource() {
-        switch settingsManager.settings.cgm {
+    func deleteGlucoseSource() {
+        cgmManager = nil
+        updateGlucoseSource(
+            cgmGlucoseSourceType: CGMType.none,
+            cgmGlucosePluginId: ""
+        )
+    }
+
+    func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
+        self.cgmGlucoseSourceType = cgmGlucoseSourceType
+        self.cgmGlucosePluginId = cgmGlucosePluginId
+
+        // if not plugin, manager is not changed and stay with the "old" value if the user come back to previous cgmtype
+        // if plugin, if the same pluginID, no change required because the manager is available
+        // if plugin, if not the same pluginID, need to reset the cgmManager
+        // if plugin and newManager provides, update cgmManager
+        debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
+        if let manager = newManager
+        {
+            cgmManager = manager
+        } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
+            cgmManager = cgmManagerFromRawValue(rawCGMManager)
+        }
+//        } else if self.cgmGlucoseSourceType == .plugin, self.cgmGlucosePluginId != , self.cgmGlucosePluginId != cgmManager?.pluginIdentifier  {
+//            cgmManager = nil
+//        }
+
+        switch self.cgmGlucoseSourceType {
+        case nil,
+             .none?:
+            glucoseSource = nil
         case .xdrip:
             glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
-        case .dexcomG5:
-            glucoseSource = dexcomSourceG5
-        case .dexcomG6:
-            glucoseSource = dexcomSourceG6
-        case .dexcomG7:
-            glucoseSource = dexcomSourceG7
         case .nightscout:
             glucoseSource = nightscoutManager
         case .simulator:
             glucoseSource = simulatorSource
-        case .libreTransmitter:
-            glucoseSource = libreSource
         case .glucoseDirect:
             glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
         case .enlite:
             glucoseSource = deviceDataManager
+        case .plugin:
+            glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
         }
         // update the config
-        cgmGlucoseSourceType = settingsManager.settings.cgm
+    }
+
+    /// Upload cgmManager from raw value
+    func cgmManagerFromRawValue(_ rawValue: [String: Any]) -> CGMManagerUI? {
+        guard let rawState = rawValue["state"] as? CGMManager.RawStateValue,
+              let cgmGlucosePluginId = self.cgmGlucosePluginId,
+              let Manager = pluginCGMManager.getCGMManagerTypeByIdentifier(cgmGlucosePluginId)
+        else {
+            return nil
+        }
+
+        return Manager.init(rawState: rawState)
     }
 
     /// function called when a callback is fired by CGM BLE - no more used
@@ -75,7 +136,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
     public func refreshCGM() {
         debug(.deviceManager, "refreshCGM by pump")
-        updateGlucoseSource()
+        // updateGlucoseSource(cgmGlucoseSourceType: settingsManager.settings.cgm, cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier)
+
         Publishers.CombineLatest3(
             Just(glucoseStorage.syncDate()),
             healthKitManager.fetch(nil),
@@ -170,8 +232,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             .receive(on: processQueue)
             .flatMap { _ -> AnyPublisher<[BloodGlucose], Never> in
                 debug(.nightscout, "FetchGlucoseManager timer heartbeat")
-                self.updateGlucoseSource()
-                return self.glucoseSource.fetch(self.timer).eraseToAnyPublisher()
+                // self.updateGlucoseSource(manager: nil)
+                if let glucoseSource = self.glucoseSource {
+                    return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
+                } else {
+                    return Empty(completeImmediately: false).eraseToAnyPublisher()
+                }
             }
             .sink { glucose in
                 debug(.nightscout, "FetchGlucoseManager callback sensor")
@@ -193,22 +259,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             .store(in: &lifetime)
         timer.fire()
         timer.resume()
-
-        UserDefaults.standard
-            .publisher(for: \.dexcomTransmitterID)
-            .removeDuplicates()
-            .sink { id in
-                if self.settingsManager.settings.cgm == .dexcomG5 {
-                    if id != self.dexcomSourceG5.transmitterID {
-                        self.dexcomSourceG5 = DexcomSourceG5(glucoseStorage: self.glucoseStorage, glucoseManager: self)
-                    }
-                } else if self.settingsManager.settings.cgm == .dexcomG6 {
-                    if id != self.dexcomSourceG6.transmitterID {
-                        self.dexcomSourceG6 = DexcomSourceG6(glucoseStorage: self.glucoseStorage, glucoseManager: self)
-                    }
-                }
-            }
-            .store(in: &lifetime)
     }
 
     func sourceInfo() -> [String: Any]? {
@@ -216,13 +266,13 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
 }
 
-extension UserDefaults {
-    @objc var dexcomTransmitterID: String? {
-        get {
-            string(forKey: "DexcomSource.transmitterID")?.nonEmpty
-        }
-        set {
-            set(newValue, forKey: "DexcomSource.transmitterID")
-        }
+extension CGMManager {
+    typealias RawValue = [String: Any]
+
+    var rawValue: [String: Any] {
+        [
+            "managerIdentifier": pluginIdentifier,
+            "state": rawState
+        ]
     }
 }

+ 277 - 0
FreeAPS/Sources/APS/PluginManager.swift

@@ -0,0 +1,277 @@
+import Foundation
+import LoopKit
+import LoopKitUI
+import Swinject
+
+protocol PluginManager {
+    var availablePumpManagers: [PumpManagerDescriptor] { get }
+    var availableCGMManagers: [CGMManagerDescriptor] { get }
+    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type?
+    func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type?
+}
+
+class BasePluginManager: Injectable, PluginManager {
+    let pluginBundles: [Bundle]
+
+    init(resolver: Resolver) {
+        let pluginsURL: URL? = Bundle.main.privateFrameworksURL
+        var bundles = [Bundle]()
+
+        if let pluginsURL = pluginsURL {
+            do {
+                for pluginURL in try FileManager.default.contentsOfDirectory(at: pluginsURL, includingPropertiesForKeys: nil)
+                    .filter({ $0.path.hasSuffix(".framework") })
+                {
+                    if let bundle = Bundle(url: pluginURL) {
+                        if let bname = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String {
+                            debug(.deviceManager, "bundle name2:\(bname)")
+                        }
+                        if let bcgm = bundle.object(forInfoDictionaryKey: "com.loopkit.Loop.CGMManagerIdentifier") as? String {
+                            debug(.deviceManager, "bundle is CGM")
+                        }
+
+                        if bundle.isLoopPlugin {
+                            debug(.deviceManager, "Found loop plugin:\(pluginURL.absoluteString)")
+                            bundles.append(bundle)
+                        }
+                    }
+                }
+            } catch {
+                debug(.deviceManager, "Error loading plugin: \(error)")
+            }
+        }
+        pluginBundles = bundles
+        injectServices(resolver)
+    }
+
+    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type? {
+        for bundle in pluginBundles {
+            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String,
+               name == identifier
+            {
+                do {
+                    try bundle.loadAndReturnError()
+
+                    if let principalClass = bundle.principalClass as? NSObject.Type {
+                        if let plugin = principalClass.init() as? PumpManagerUIPlugin {
+                            return plugin.pumpManagerType
+                        } else {
+                            fatalError("PrincipalClass does not conform to PumpManagerUIPlugin")
+                        }
+
+                    } else {
+                        fatalError("PrincipalClass not found")
+                    }
+                } catch {
+                    debug(.deviceManager, "Error loading plugin: \(error)")
+                }
+            }
+        }
+        return nil
+    }
+
+    var availablePumpManagers: [PumpManagerDescriptor] {
+        pluginBundles.compactMap({ (bundle) -> PumpManagerDescriptor? in
+            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerDisplayName.rawValue) as? String,
+                  let identifier = bundle
+                  .object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String
+            else {
+                return nil
+            }
+
+            return PumpManagerDescriptor(identifier: identifier, localizedTitle: title)
+        })
+    }
+
+    func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type? {
+        for bundle in pluginBundles {
+            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String,
+               name == identifier
+            {
+                do {
+                    try bundle.loadAndReturnError()
+
+                    if let principalClass = bundle.principalClass as? NSObject.Type {
+                        if let plugin = principalClass.init() as? CGMManagerUIPlugin {
+                            return plugin.cgmManagerType
+                        } else {
+                            fatalError("PrincipalClass does not conform to CGMManagerUIPlugin")
+                        }
+
+                    } else {
+                        fatalError("PrincipalClass not found")
+                    }
+                } catch {
+                    debug(.deviceManager, "Error loading plugin: \(error)")
+                }
+            }
+        }
+        return nil
+    }
+
+    var availableCGMManagers: [CGMManagerDescriptor] {
+        pluginBundles.compactMap({ (bundle) -> CGMManagerDescriptor? in
+            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerDisplayName.rawValue) as? String,
+                  let identifier = bundle
+                  .object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String
+            else {
+                return nil
+            }
+
+            return CGMManagerDescriptor(identifier: identifier, localizedTitle: title)
+        })
+    }
+
+    func getServiceTypeByIdentifier(_ identifier: String) -> ServiceUI.Type? {
+        for bundle in pluginBundles {
+            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String,
+               name == identifier
+            {
+                do {
+                    try bundle.loadAndReturnError()
+
+                    if let principalClass = bundle.principalClass as? NSObject.Type {
+                        if let plugin = principalClass.init() as? ServiceUIPlugin {
+                            return plugin.serviceType
+                        } else {
+                            fatalError("PrincipalClass does not conform to ServiceUIPlugin")
+                        }
+
+                    } else {
+                        fatalError("PrincipalClass not found")
+                    }
+                } catch {
+                    debug(.deviceManager, "Error loading plugin: \(error)")
+                }
+            }
+        }
+        return nil
+    }
+
+    var availableServices: [ServiceDescriptor] {
+        pluginBundles.compactMap({ (bundle) -> ServiceDescriptor? in
+            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceDisplayName.rawValue) as? String,
+                  let identifier = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String
+            else {
+                return nil
+            }
+
+            return ServiceDescriptor(identifier: identifier, localizedTitle: title)
+        })
+    }
+
+    func getStatefulPluginTypeByIdentifier(_ identifier: String) -> StatefulPluggable.Type? {
+        for bundle in pluginBundles {
+            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String,
+               name == identifier
+            {
+                do {
+                    try bundle.loadAndReturnError()
+
+                    if let principalClass = bundle.principalClass as? NSObject.Type {
+                        if let plugin = principalClass.init() as? StatefulPlugin {
+                            return plugin.pluginType
+                        } else {
+                            fatalError("PrincipalClass does not conform to StatefulPlugin")
+                        }
+
+                    } else {
+                        fatalError("PrincipalClass not found")
+                    }
+                } catch {
+                    debug(.deviceManager, "Error loading plugin: \(error)")
+                }
+            }
+        }
+        return nil
+    }
+
+    var availableStatefulPluginIdentifiers: [String] {
+        pluginBundles.compactMap({ (bundle) -> String? in
+            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String
+        })
+    }
+
+    func getOnboardingTypeByIdentifier(_ identifier: String) -> OnboardingUI.Type? {
+        for bundle in pluginBundles {
+            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String,
+               name == identifier
+            {
+                do {
+                    try bundle.loadAndReturnError()
+
+                    if let principalClass = bundle.principalClass as? NSObject.Type {
+                        if let plugin = principalClass.init() as? OnboardingUIPlugin {
+                            return plugin.onboardingType
+                        } else {
+                            fatalError("PrincipalClass does not conform to OnboardingUIPlugin")
+                        }
+
+                    } else {
+                        fatalError("PrincipalClass not found")
+                    }
+                } catch {
+                    debug(.deviceManager, "Error loading plugin: \(error)")
+                }
+            }
+        }
+        return nil
+    }
+
+    var availableOnboardingIdentifiers: [String] {
+        pluginBundles.compactMap({ (bundle) -> String? in
+            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String
+        })
+    }
+
+    func getSupportUITypeByIdentifier(_ identifier: String) -> SupportUI.Type? {
+        for bundle in pluginBundles {
+            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String,
+               name == identifier
+            {
+                do {
+                    try bundle.loadAndReturnError()
+
+                    if let principalClass = bundle.principalClass as? NSObject.Type {
+                        if let plugin = principalClass.init() as? SupportUIPlugin {
+                            return type(of: plugin.support)
+                        } else {
+                            fatalError("PrincipalClass does not conform to SupportUIPlugin")
+                        }
+
+                    } else {
+                        fatalError("PrincipalClass not found")
+                    }
+                } catch {
+                    debug(.deviceManager, "Error loading plugin: \(error)")
+                }
+            }
+        }
+        return nil
+    }
+}
+
+extension Bundle {
+    var isPumpManagerPlugin: Bool {
+        object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String != nil }
+
+    var isCGMManagerPlugin: Bool {
+        object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String != nil }
+
+    var isStatefulPlugin: Bool {
+        object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String != nil }
+
+    var isServicePlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String != nil }
+    var isOnboardingPlugin: Bool {
+        object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String != nil }
+
+    var isSupportPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String != nil }
+
+    var isLoopPlugin: Bool {
+        isPumpManagerPlugin || isCGMManagerPlugin || isStatefulPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin
+    }
+
+    var isLoopExtension: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.extensionIdentifier.rawValue) as? String != nil }
+
+    var isSimulator: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.pluginIsSimulator.rawValue) as? Bool == true }
+}

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

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

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

@@ -9,5 +9,6 @@ final class APSAssembly: Assembly {
         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) }
+        container.register(PluginManager.self) { r in BasePluginManager(resolver: r) }
     }
 }

+ 50 - 0
FreeAPS/Sources/Helpers/PropertyWrappers/PersistedProperty.swift

@@ -48,3 +48,53 @@ import Foundation
         }
     }
 }
+
+@propertyWrapper public struct PersistedProperty<Value> {
+    let key: String
+    let storageURL: URL
+
+    public init(key: String) {
+        self.key = key
+
+        let documents: URL
+
+        guard let localDocuments = try? FileManager.default.url(
+            for: .documentDirectory,
+            in: .userDomainMask,
+            appropriateFor: nil,
+            create: true
+        ) else {
+            preconditionFailure("Could not get a documents directory URL.")
+        }
+        documents = localDocuments
+        storageURL = documents.appendingPathComponent(key + ".plist")
+    }
+
+    public var wrappedValue: Value? {
+        get {
+            do {
+                let data = try Data(contentsOf: storageURL)
+
+                guard let value = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? Value
+                else {
+                    return nil
+                }
+                return value
+            } catch {}
+            return nil
+        }
+        set {
+            guard let newValue = newValue else {
+                do {
+                    try FileManager.default.removeItem(at: storageURL)
+                } catch {}
+                return
+            }
+            do {
+                let data = try PropertyListSerialization.data(fromPropertyList: newValue, format: .binary, options: 0)
+                try data.write(to: storageURL, options: .atomic)
+
+            } catch {}
+        }
+    }
+}

+ 6 - 1
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -12,7 +12,8 @@ struct FreeAPSSettings: JSON, Equatable {
     var insulinReqPercentage: Decimal = 70
     var skipBolusScreenAfterCarbs: Bool = false
     var displayHR: Bool = false
-    var cgm: CGMType = .nightscout
+    var cgm: CGMType = .none
+    var cgmPluginIdentifier: String = ""
     var uploadGlucose: Bool = true
     var useCalendar: Bool = false
     var glucoseBadge: Bool = false
@@ -105,6 +106,10 @@ extension FreeAPSSettings: Decodable {
             settings.cgm = cgm
         }
 
+        if let cgmPluginIdentifier = try? container.decode(String.self, forKey: .cgmPluginIdentifier) {
+            settings.cgmPluginIdentifier = cgmPluginIdentifier
+        }
+
         if let uploadGlucose = try? container.decode(Bool.self, forKey: .uploadGlucose) {
             settings.uploadGlucose = uploadGlucose
         }

+ 77 - 16
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -4,24 +4,67 @@ import G7SensorKit
 import LoopKitUI
 import SwiftUI
 
+struct cgmName: Identifiable, Hashable {
+    var id: String
+    var type: CGMType
+    var displayName: String
+    var subtitle: String
+}
+
+let cgmDefaultName = cgmName(
+    id: CGMType.none.id,
+    type: .none,
+    displayName: CGMType.none.displayName,
+    subtitle: CGMType.none.subtitle
+)
+
 extension CGM {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var cgmManager: FetchGlucoseManager!
         @Injected() var calendarManager: CalendarManager!
+        @Injected() var pluginCGMManager: PluginManager!
 
         @Published var setupCGM: Bool = false
-        @Published var cgm: CGMType = .nightscout
-        // @Published var transmitterID = ""
-        @Published var uploadGlucose = true
+        @Published var cgmCurrent = cgmDefaultName
         @Published var smoothGlucose = false
         @Published var createCalendarEvents = false
         @Published var calendarIDs: [String] = []
         @Published var currentCalendarID: String = ""
         @Persisted(key: "CalendarManager.currentCalendarID") var storedCalendarID: String? = nil
         @Published var cgmTransmitterDeviceAddress: String? = nil
+        @Published var listOfCGM: [cgmName] = []
 
         override func subscribe() {
-            cgm = settingsManager.settings.cgm
+            // collect the list of CGM available with plugins and CGMType defined manually
+            listOfCGM = CGMType.allCases.filter { $0 != CGMType.plugin }.map {
+                cgmName(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
+            } +
+                pluginCGMManager.availableCGMManagers.map {
+                    cgmName(id: $0.identifier, type: CGMType.plugin, displayName: $0.localizedTitle, subtitle: $0.localizedTitle)
+                }
+
+            switch settingsManager.settings.cgm {
+            case .plugin:
+                if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
+                    cgmCurrent = cgmName(
+                        id: settingsManager.settings.cgmPluginIdentifier,
+                        type: .plugin,
+                        displayName: cgmPluginInfo.displayName,
+                        subtitle: cgmPluginInfo.subtitle
+                    )
+                } else {
+                    // no more type of plugin available - restart to defaut
+                    cgmCurrent = cgmDefaultName
+                }
+            default:
+                cgmCurrent = cgmName(
+                    id: settingsManager.settings.cgm.id,
+                    type: settingsManager.settings.cgm,
+                    displayName: settingsManager.settings.cgm.displayName,
+                    subtitle: settingsManager.settings.cgm.subtitle
+                )
+            }
+
             currentCalendarID = storedCalendarID ?? ""
             calendarIDs = calendarManager.calendarIDs()
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
@@ -29,7 +72,7 @@ extension CGM {
             subscribeSetting(\.useCalendar, on: $createCalendarEvents) { createCalendarEvents = $0 }
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
 
-            $cgm
+            $cgmCurrent
                 .removeDuplicates()
                 .sink { [weak self] value in
                     guard let self = self else { return }
@@ -37,7 +80,16 @@ extension CGM {
                         self.settingsManager.settings.cgm = .nightscout
                         return
                     }
-                    self.settingsManager.settings.cgm = value
+                    if value.type != self.settingsManager.settings.cgm ||
+                        value.id != self.settingsManager.settings.cgmPluginIdentifier
+                    {
+                        self.settingsManager.settings.cgm = value.type
+                        self.settingsManager.settings.cgmPluginIdentifier = value.id
+                        self.cgmManager.updateGlucoseSource(
+                            cgmGlucoseSourceType: value.type,
+                            cgmGlucosePluginId: value.id
+                        )
+                    }
                 }
                 .store(in: &lifetime)
 
@@ -72,26 +124,35 @@ extension CGM {
 extension CGM.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
         setupCGM = false
+
         // if CGM was deleted
         if cgmManager.cgmGlucoseSourceType == nil {
-            cgm = .nightscout
+            cgmCurrent = cgmDefaultName
+            settingsManager.settings.cgm = cgmDefaultName.type
+            settingsManager.settings.cgmPluginIdentifier = cgmDefaultName.id
+            cgmManager.deleteGlucoseSource()
+        } else {
+            cgmManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
         }
+
         // refresh the upload options
-        uploadGlucose = settingsManager.settings.uploadGlucose
-        cgmManager.updateGlucoseSource()
+        settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
+
+        // update if required the Glucose source
     }
 }
 
 extension CGM.StateModel: CGMManagerOnboardingDelegate {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
-        // Possibility add the dexcom number !
-        if let dexcomG6Manager: G6CGMManager = manager as? G6CGMManager {
-            UserDefaults.standard.dexcomTransmitterID = dexcomG6Manager.transmitter.ID
+        // update the setting of upload Glucose in services
+        settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
 
-        } else if let dexcomG5Manager: G5CGMManager = manager as? G5CGMManager {
-            UserDefaults.standard.dexcomTransmitterID = dexcomG5Manager.transmitter.ID
-        }
-        cgmManager.updateGlucoseSource()
+        // update the glucose source
+        cgmManager.updateGlucoseSource(
+            cgmGlucoseSourceType: cgmCurrent.type,
+            cgmGlucosePluginId: cgmCurrent.id,
+            newManager: manager
+        )
     }
 
     func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {

+ 14 - 9
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -14,28 +14,28 @@ extension CGM {
             NavigationView {
                 Form {
                     Section(header: Text("CGM")) {
-                        Picker("Type", selection: $state.cgm) {
-                            ForEach(CGMType.allCases) { type in
+                        Picker("Type", selection: $state.cgmCurrent) {
+                            ForEach(state.listOfCGM) { type in
                                 VStack(alignment: .leading) {
                                     Text(type.displayName)
                                     Text(type.subtitle).font(.caption).foregroundColor(.secondary)
                                 }.tag(type)
                             }
                         }
-                        if let link = state.cgm.externalLink {
+                        if let link = state.cgmCurrent.type.externalLink {
                             Button("About this source") {
                                 UIApplication.shared.open(link, options: [:], completionHandler: nil)
                             }
                         }
                     }
-                    if [.dexcomG5, .dexcomG6, .dexcomG7, .libreTransmitter].contains(state.cgm) {
+                    if state.cgmCurrent.type == .plugin {
                         Section {
                             Button("CGM Configuration") {
                                 setupCGM.toggle()
                             }
                         }
                     }
-                    if state.cgm == .xdrip {
+                    if state.cgmCurrent.type == .xdrip {
                         Section(header: Text("Heartbeat")) {
                             VStack(alignment: .leading) {
                                 if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
@@ -67,20 +67,25 @@ extension CGM {
                 .navigationTitle("CGM")
                 .navigationBarTitleDisplayMode(.automatic)
                 .sheet(isPresented: $setupCGM) {
-                    if let cgmFetchManager = state.cgmManager, cgmFetchManager.glucoseSource.cgmType == state.cgm {
+                    if let cgmFetchManager = state.cgmManager,
+                       let cgmManager = cgmFetchManager.cgmManager,
+                       state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
+                       state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
+                    {
                         CGMSettingsView(
-                            cgmManager: cgmFetchManager.glucoseSource.cgmManager!,
+                            cgmManager: cgmManager,
                             bluetoothManager: state.provider.apsManager.bluetoothManager!,
                             unit: state.settingsManager.settings.units,
                             completionDelegate: state
                         )
                     } else {
                         CGMSetupView(
-                            CGMType: state.cgm,
+                            CGMType: state.cgmCurrent,
                             bluetoothManager: state.provider.apsManager.bluetoothManager!,
                             unit: state.settingsManager.settings.units,
                             completionDelegate: state,
-                            setupDelegate: state
+                            setupDelegate: state,
+                            pluginCGMManager: self.state.pluginCGMManager
                         )
                     }
                 }

+ 3 - 1
FreeAPS/Sources/Modules/CGM/View/CGMSettingsView.swift

@@ -5,7 +5,7 @@ import UIKit
 
 extension CGM {
     struct CGMSettingsView: UIViewControllerRepresentable {
-        let cgmManager: CGMManagerUI
+        let cgmManager: CGMManagerUI?
         let bluetoothManager: BluetoothStateManager
         let unit: GlucoseUnits
         weak var completionDelegate: CompletionDelegate?
@@ -19,6 +19,8 @@ extension CGM {
                 displayGlucosePreference = DisplayGlucosePreference(displayGlucoseUnit: .millimolesPerLiter)
             }
 
+            guard let cgmManager = cgmManager else { return UIViewController() }
+
             var vc = cgmManager.settingsViewController(
                 bluetoothProvider: bluetoothManager,
                 displayGlucosePreference: displayGlucosePreference,

+ 9 - 33
FreeAPS/Sources/Modules/CGM/View/CGMSetupView.swift

@@ -1,9 +1,3 @@
-import CGMBLEKit
-import CGMBLEKitUI
-import G7SensorKit
-import G7SensorKitUI
-import LibreTransmitter
-import LibreTransmitterUI
 import LoopKit
 import LoopKitUI
 import SwiftUI
@@ -11,11 +5,12 @@ import UIKit
 
 extension CGM {
     struct CGMSetupView: UIViewControllerRepresentable {
-        let CGMType: CGMType
+        let CGMType: cgmName
         let bluetoothManager: BluetoothStateManager
         let unit: GlucoseUnits
         weak var completionDelegate: CompletionDelegate?
         weak var setupDelegate: CGMManagerOnboardingDelegate?
+        let pluginCGMManager: PluginManager
 
         func makeUIViewController(context _: UIViewControllerRepresentableContext<CGMSetupView>) -> UIViewController {
             var setupViewController: SetupUIResult<
@@ -31,38 +26,19 @@ extension CGM {
                 displayGlucosePreference = DisplayGlucosePreference(displayGlucoseUnit: .millimolesPerLiter)
             }
 
-            switch CGMType {
-            case .dexcomG5:
-                setupViewController = G5CGMManager.setupViewController(
-                    bluetoothProvider: bluetoothManager,
-                    displayGlucosePreference: displayGlucosePreference,
-                    colorPalette: .default,
-                    allowDebugFeatures: false
-                )
-            case .dexcomG6:
-                setupViewController = G6CGMManager.setupViewController(
-                    bluetoothProvider: bluetoothManager,
-                    displayGlucosePreference: displayGlucosePreference,
-                    colorPalette: .default,
-                    allowDebugFeatures: false
-                )
-            case .dexcomG7:
-                setupViewController =
-                    G7CGMManager.setupViewController(
+            switch CGMType.type {
+            case .plugin:
+                if let cgmManagerUIType = pluginCGMManager.getCGMManagerTypeByIdentifier(CGMType.id) {
+                    setupViewController = cgmManagerUIType.setupViewController(
                         bluetoothProvider: bluetoothManager,
                         displayGlucosePreference: displayGlucosePreference,
                         colorPalette: .default,
                         allowDebugFeatures: false,
                         prefersToSkipUserInteraction: false
                     )
-            case .libreTransmitter:
-                setupViewController = LibreTransmitterManagerV3.setupViewController(
-                    bluetoothProvider: bluetoothManager,
-                    displayGlucosePreference: displayGlucosePreference,
-                    colorPalette: .default,
-                    allowDebugFeatures: false,
-                    prefersToSkipUserInteraction: false
-                )
+                } else {
+                    break
+                }
             default:
                 break
             }

+ 3 - 13
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -1,7 +1,5 @@
-import CGMBLEKit
 import Combine
 import CoreData
-import G7SensorKit
 import LoopKit
 import SwiftDate
 import SwiftUI
@@ -26,6 +24,7 @@ extension NightscoutConfig {
         @Published var isUploadEnabled = false // Allow uploads
         @Published var uploadStats = false // Upload Statistics
         @Published var uploadGlucose = true // Upload Glucose
+        @Published var changeUploadGlucose = true // if plugin, need to be change in CGM configuration
         @Published var useLocalSource = false
         @Published var localPort: Decimal = 0
         @Published var units: GlucoseUnits = .mmolL
@@ -41,23 +40,14 @@ extension NightscoutConfig {
             dia = settingsManager.pumpSettings.insulinActionCurve
             maxBasal = settingsManager.pumpSettings.maxBasal
             maxBolus = settingsManager.pumpSettings.maxBolus
+            changeUploadGlucose = (cgmManager.cgmGlucoseSourceType != CGMType.plugin)
 
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.useLocalGlucoseSource, on: $useLocalSource) { useLocalSource = $0 }
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
             subscribeSetting(\.uploadStats, on: $uploadStats) { uploadStats = $0 }
-            subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 }, didSet: { val in
-                if let cgmManagerG5 = self.cgmManager.glucoseSource.cgmManager as? G5CGMManager {
-                    cgmManagerG5.shouldSyncToRemoteService = val
-                }
-                if let cgmManagerG6 = self.cgmManager.glucoseSource.cgmManager as? G6CGMManager {
-                    cgmManagerG6.shouldSyncToRemoteService = val
-                }
-                if let cgmManagerG7 = self.cgmManager.glucoseSource.cgmManager as? G7CGMManager {
-                    cgmManagerG7.uploadReadings = val
-                }
-            })
+            subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
         }
 
         func connect() {

+ 1 - 1
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -58,7 +58,7 @@ extension NightscoutConfig {
                     Toggle("Upload", isOn: $state.isUploadEnabled)
                     if state.isUploadEnabled {
                         Toggle("Statistics", isOn: $state.uploadStats)
-                        Toggle("Glucose", isOn: $state.uploadGlucose)
+                        Toggle("Glucose", isOn: $state.uploadGlucose).disabled(!state.changeUploadGlucose)
                     }
                 } header: {
                     Text("Allow Uploads")

+ 0 - 1
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -472,7 +472,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
 
     var glucoseManager: FetchGlucoseManager?
     var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .nightscout
 
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         Future { [weak self] promise in

+ 0 - 1
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -131,7 +131,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
     var glucoseManager: FetchGlucoseManager?
     var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .nightscout
 
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         fetchGlucose(since: glucoseStorage.syncDate())

+ 41 - 0
scripts/copy-plugins.sh

@@ -0,0 +1,41 @@
+#!/bin/sh -e
+
+#  copy-plugins.sh
+#  Loop
+#
+#  Copyright © 2019 LoopKit Authors. All rights reserved.
+
+
+shopt -s nullglob
+
+# Copy device plugins
+function copy_plugins {
+    echo "Looking for plugins in $1"
+    for f in "$1"/*.loopplugin; do
+      plugin=$(basename "$f")
+      echo Copying plugin: $plugin to frameworks directory in app
+      plugin_path="$(readlink -f "$f" || echo "$f")"
+      plugin_as_framework_path="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${plugin%.*}.framework"
+      rsync -va --exclude=Frameworks "$plugin_path/." "${plugin_as_framework_path}"
+      # Rename .plugin to .framework
+      if [ "$EXPANDED_CODE_SIGN_IDENTITY" != "-" ] && [ "$EXPANDED_CODE_SIGN_IDENTITY" != "" ]; then
+        export CODESIGN_ALLOCATE=${DT_TOOLCHAIN_DIR}/usr/bin/codesign_allocate
+        echo "Signing ${plugin} with ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
+        /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none --preserve-metadata=identifier,entitlements,flags "$plugin_as_framework_path"
+      else
+        echo "Skipping signing, no identity set"
+      fi
+      for framework_path in "${f}"/Frameworks/*.framework; do
+        framework=$(basename "$framework_path")
+        echo "Copying plugin's framework $framework_path to ${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/."
+        cp -avf "$framework_path" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/."
+        plugin_path="$(readlink -f "$f" || echo "$f")"
+        if [ "$EXPANDED_CODE_SIGN_IDENTITY" != "-" ] && [ "$EXPANDED_CODE_SIGN_IDENTITY" != "" ]; then
+          echo "Signing $framework for $plugin with $EXPANDED_CODE_SIGN_IDENTITY_NAME"
+          /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none --preserve-metadata=identifier,entitlements,flags "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${framework}"
+        fi
+      done
+    done
+}
+
+copy_plugins "$BUILT_PRODUCTS_DIR"