Parcourir la source

Fetch BG :
- Change the Dexcom and LibreTransmitter mechanism with the use of BLE callback and not only the tick heartbeat by timer
- Allow to activate the heartbeat with pump (change the bool of heartbeatBypump)
- no more max delay to ensure the pump update (for Medtronic)

minor changes :
- improve logs in loop frameworks
- some stats storage in async mode
-

avouspierre il y a 3 ans
Parent
commit
cd9e234fa4

+ 21 - 9
Dependencies/CGMBLEKit/CGMBLEKit/OSLog.swift

@@ -49,11 +49,14 @@ extension OSLog {
     }
     }
 
 
     func debug(_ message: StaticString, _ args: CVarArg...) {
     func debug(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
-                    msg,
+                    {msg}(),
                     printToConsole: true,
                     printToConsole: true,
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,
@@ -64,11 +67,14 @@ extension OSLog {
     }
     }
 
 
     func info(_ message: StaticString, _ args: CVarArg...) {
     func info(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.info(
                 category.logger.info(
-                    msg,
+                    {msg}(),
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,
                     line: #line
                     line: #line
@@ -78,11 +84,14 @@ extension OSLog {
     }
     }
 
 
     func `default`(_ message: StaticString, _ args: CVarArg...) {
     func `default`(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
-                    msg,
+                    {msg}(),
                     printToConsole: true,
                     printToConsole: true,
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,
@@ -93,13 +102,16 @@ extension OSLog {
     }
     }
 
 
     func error(_ message: StaticString, _ args: CVarArg...) {
     func error(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
            
            
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.warning(
                 category.logger.warning(
-                    msg,
-                    description: message.debugDescription,
+                    {msg}(),
+                    description:  {msg}(),
                     error: nil,
                     error: nil,
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,

+ 364 - 5
Dependencies/G7SensorKit/G7SensorKit/OSLog.swift

@@ -1,32 +1,119 @@
+
 //
 //
 //  OSLog.swift
 //  OSLog.swift
-//  Loop
+//  OmniBLE
 //
 //
 //  Copyright © 2017 LoopKit Authors. All rights reserved.
 //  Copyright © 2017 LoopKit Authors. All rights reserved.
+// OSLog updated for FreeAPSX logs
 //
 //
 
 
 import os.log
 import os.log
+import Foundation
+
 
 
+let loggerLock = NSRecursiveLock()
+let baseReporter: IssueReporter = SimpleLogReporter()
+let category = Logger.Category.G7SensorKit
+
+extension NSLocking {
+    func perform<T>(_ block: () throws -> T) rethrows -> T {
+        lock()
+        defer { unlock() }
+        return try block()
+    }
+}
+
+extension NSRecursiveLock {
+    convenience init(label: String) {
+        self.init()
+        name = label
+    }
+}
+
+extension NSLock {
+    convenience init(label: String) {
+        self.init()
+        name = label
+    }
+}
 
 
 extension OSLog {
 extension OSLog {
+    
     convenience init(category: String) {
     convenience init(category: String) {
         self.init(subsystem: "org.loopkit.G7SensorKit", category: category)
         self.init(subsystem: "org.loopkit.G7SensorKit", category: category)
     }
     }
 
 
     func debug(_ message: StaticString, _ args: CVarArg...) {
     func debug(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .debug, args)
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
+        DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
+            loggerLock.perform {
+                category.logger.debug(
+                    {msg}(),
+                    printToConsole: true,
+                    file: #file,
+                    function: #function,
+                    line: #line
+                )
+            }
+        }.perform()
     }
     }
 
 
     func info(_ message: StaticString, _ args: CVarArg...) {
     func info(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .info, args)
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
+        DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
+            loggerLock.perform {
+                category.logger.info(
+                    {msg}(),
+                    file: #file,
+                    function: #function,
+                    line: #line
+                )
+            }
+        }.perform()
     }
     }
 
 
     func `default`(_ message: StaticString, _ args: CVarArg...) {
     func `default`(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .default, args)
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
+        DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
+            loggerLock.perform {
+                category.logger.debug(
+                    {msg}(),
+                    printToConsole: true,
+                    file: #file,
+                    function: #function,
+                    line: #line
+                )
+            }
+        }.perform()
     }
     }
 
 
     func error(_ message: StaticString, _ args: CVarArg...) {
     func error(_ message: StaticString, _ args: CVarArg...) {
-        log(message, type: .error, args)
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
+        DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
+           
+            loggerLock.perform {
+                category.logger.warning(
+                    {msg}(),
+                    description: message.description,
+                    error: nil,
+                    file: #file,
+                    function: #function,
+                    line: #line
+                )
+            }
+        }.perform()
     }
     }
 
 
     private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) {
     private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) {
@@ -48,3 +135,275 @@ extension OSLog {
         }
         }
     }
     }
 }
 }
+
+protocol IssueReporter: AnyObject {
+    /// Call this method in `applicationDidFinishLaunching()`.
+    func setup()
+
+    func setUserIdentifier(_: String?)
+
+    func reportNonFatalIssue(withName: String, attributes: [String: String])
+
+    func reportNonFatalIssue(withError: NSError)
+
+    func log(_ category: String, _ message: String, file: String, function: String, line: UInt)
+}
+
+final class Logger {
+    static let `default` = Logger(category: .default, reporter: baseReporter)
+    static let  G7SensorKit = Logger(category: .G7SensorKit, reporter: baseReporter)
+
+    enum Category: String {
+        case `default`
+        case G7SensorKit
+
+        var name: String {
+            rawValue
+        }
+
+        var logger: Logger {
+            switch self {
+            case .default: return .default
+            case .G7SensorKit: return .G7SensorKit
+            
+            }
+        }
+
+        fileprivate var log: OSLog {
+            let subsystem = Bundle.main.bundleIdentifier!
+            switch self {
+            case .default: return OSLog.default
+            case .G7SensorKit: return OSLog(subsystem: subsystem, category: name)
+            }
+        }
+    }
+
+    fileprivate enum Error: Swift.Error {
+        case error(String)
+        case errorWithInnerError(String, Swift.Error)
+        case errorWithDescription(String, String)
+        case errorWithDescriptionAndInnerError(String, String, Swift.Error)
+
+        private func domain() -> String {
+            switch self {
+            case let .error(domain),
+                 let .errorWithDescription(domain, _),
+                 let .errorWithDescriptionAndInnerError(domain, _, _),
+                 let .errorWithInnerError(domain, _):
+                return domain
+            }
+        }
+
+        private func innerError() -> Swift.Error? {
+            switch self {
+            case let .errorWithDescriptionAndInnerError(_, _, error),
+                 let .errorWithInnerError(_, error):
+                return error
+            default: return nil
+            }
+        }
+
+        func asNSError() -> NSError {
+            var info: [String: Any] = ["Description": String(describing: self)]
+
+            if let error = innerError() {
+                info["Error"] = String(describing: error)
+            }
+
+            return NSError(domain: domain(), code: -1, userInfo: info)
+        }
+    }
+
+    private let category: Category
+    private let reporter: IssueReporter
+    let log: OSLog
+
+    private init(category: Category, reporter: IssueReporter) {
+        self.category = category
+        self.reporter = reporter
+        log = category.log
+    }
+
+    static func setup() {
+        loggerLock.perform {
+            baseReporter.setup()
+        }
+    }
+
+    func debug(
+        _ message: @autoclosure () -> String,
+        printToConsole: Bool = true,
+        file: String = #file,
+        function: String = #function,
+        line: UInt = #line
+    ) {
+        let message = "DEV: \(message())"
+        if printToConsole {
+            os_log("%@ - %@ - %d %{public}@", log: log, type: .debug, file.file, function, line, message)
+        }
+        reporter.log(category.name, message, file: file, function: function, line: line)
+    }
+
+    func info(
+        _ message: String,
+        file: String = #file,
+        function: String = #function,
+        line: UInt = #line
+    ) {
+        let printedMessage = "INFO: \(message)"
+        os_log("%@ - %@ - %d %{public}@", log: log, type: .info, file.file, function, line, printedMessage)
+        reporter.log(category.name, printedMessage, file: file, function: function, line: line)
+    }
+
+    func warning(
+        _ message: String,
+        description: String? = nil,
+        error maybeError: Swift.Error? = nil,
+        file: String = #file,
+        function: String = #function,
+        line: UInt = #line
+    ) {
+        let loggerError = maybeError.loggerError(message: message, withDescription: description)
+        let message = "WARN: \(String(describing: loggerError))"
+
+        os_log("%@ - %@ - %d %{public}@", log: log, type: .default, file.file, function, line, message)
+        reporter.log(category.name, message, file: file, function: function, line: line)
+        reporter.reportNonFatalIssue(withError: loggerError.asNSError())
+        
+    }
+
+    func error(
+        _ message: String,
+        description: String? = nil,
+        error maybeError: Swift.Error? = nil,
+        file: String = #file,
+        function: String = #function,
+        line: UInt = #line
+    ) -> Never {
+        errorWithoutFatalError(message, description: description, error: maybeError, file: file, function: function, line: line)
+
+        fatalError(
+            "\(message) @ \(String(describing: description)) @ \(String(describing: maybeError)) @ \(file) @ \(function) @ \(line)"
+        )
+    }
+
+
+    fileprivate func errorWithoutFatalError(
+        _ message: String,
+        description: String? = nil,
+        error maybeError: Swift.Error? = nil,
+        file: String = #file,
+        function: String = #function,
+        line: UInt = #line
+    ) {
+        let loggerError = maybeError.loggerError(message: message, withDescription: description)
+        let message = "ERR: \(String(describing: loggerError))"
+
+        os_log("%@ - %@ - %d %{public}@", log: log, type: .error, file.file, function, line, message)
+        reporter.log(category.name, message, file: file, function: function, line: line)
+        reporter.reportNonFatalIssue(withError: loggerError.asNSError())
+    }
+}
+
+private extension Optional where Wrapped == Swift.Error {
+    func loggerError(message: String, withDescription description: String?) -> Logger.Error {
+        switch (description, self) {
+        case (nil, nil):
+            return .error(message)
+        case let (descr?, nil):
+            return .errorWithDescription(message, descr)
+        case let (nil, error?):
+            return .errorWithInnerError(message, error)
+        case let (descr?, error?):
+            return .errorWithDescriptionAndInnerError(message, descr, error)
+        }
+    }
+}
+
+
+final class SimpleLogReporter: IssueReporter {
+    private let fileManager = FileManager.default
+
+    private var dateFormatter: DateFormatter {
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
+        return dateFormatter
+    }
+
+    func setup() {}
+
+    func setUserIdentifier(_: String?) {}
+
+    func reportNonFatalIssue(withName _: String, attributes _: [String: String]) {}
+
+    func reportNonFatalIssue(withError _: NSError) {}
+
+    func log(_ category: String, _ message: String, file: String, function: String, line: UInt) {
+        let now = Date()
+        let startOfDay = Calendar.current.startOfDay(for: now)
+
+        if !fileManager.fileExists(atPath: SimpleLogReporter.logDir) {
+            try? fileManager.createDirectory(
+                atPath: SimpleLogReporter.logDir,
+                withIntermediateDirectories: false,
+                attributes: nil
+            )
+        }
+
+        if !fileManager.fileExists(atPath: SimpleLogReporter.logFile) {
+            createFile(at: startOfDay)
+        } else {
+            if let attributes = try? fileManager.attributesOfItem(atPath: SimpleLogReporter.logFile),
+               let creationDate = attributes[.creationDate] as? Date, creationDate < startOfDay
+            {
+                try? fileManager.removeItem(atPath: SimpleLogReporter.logFilePrev)
+                try? fileManager.moveItem(atPath: SimpleLogReporter.logFile, toPath: SimpleLogReporter.logFilePrev)
+                createFile(at: startOfDay)
+            }
+        }
+
+        let logEntry = "\(dateFormatter.string(from: now)) [\(category)] \(file.file) - \(function) - \(line) - \(message)\n"
+        let data = logEntry.data(using: .utf8)!
+        try? data.append(fileURL: URL(fileURLWithPath: SimpleLogReporter.logFile))
+    }
+
+    private func createFile(at date: Date) {
+        fileManager.createFile(atPath: SimpleLogReporter.logFile, contents: nil, attributes: [.creationDate: date])
+    }
+
+    static var logFile: String {
+        getDocumentsDirectory().appendingPathComponent("logs/log.txt").path
+    }
+
+    static var logDir: String {
+        getDocumentsDirectory().appendingPathComponent("logs").path
+    }
+
+    static var logFilePrev: String {
+        getDocumentsDirectory().appendingPathComponent("logs/log_prev.txt").path
+    }
+
+    static func getDocumentsDirectory() -> URL {
+        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
+        let documentsDirectory = paths[0]
+        return documentsDirectory
+    }
+}
+
+private extension Data {
+    func append(fileURL: URL) throws {
+        if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
+            defer {
+                fileHandle.closeFile()
+            }
+            fileHandle.seekToEndOfFile()
+            fileHandle.write(self)
+        } else {
+            try write(to: fileURL, options: .atomic)
+        }
+    }
+}
+
+private extension String {
+    var file: String { components(separatedBy: "/").last ?? "" }
+}

+ 16 - 4
Dependencies/LoopKit/Extensions/OSLog.swift

@@ -49,7 +49,10 @@ extension OSLog {
     }
     }
 
 
     func debug(_ message: StaticString, _ args: CVarArg...) {
     func debug(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
@@ -64,7 +67,10 @@ extension OSLog {
     }
     }
 
 
     func info(_ message: StaticString, _ args: CVarArg...) {
     func info(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.info(
                 category.logger.info(
@@ -78,7 +84,10 @@ extension OSLog {
     }
     }
 
 
     func `default`(_ message: StaticString, _ args: CVarArg...) {
     func `default`(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
@@ -93,7 +102,10 @@ extension OSLog {
     }
     }
 
 
     func error(_ message: StaticString, _ args: CVarArg...) {
     func error(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
            
            
             loggerLock.perform {
             loggerLock.perform {

+ 21 - 9
Dependencies/OmniBLE/Common/OSLog.swift

@@ -43,11 +43,14 @@ extension OSLog {
     }
     }
 
 
     func debug(_ message: StaticString, _ args: CVarArg...) {
     func debug(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
-                    msg,
+                    {msg}(),
                     printToConsole: true,
                     printToConsole: true,
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,
@@ -58,11 +61,14 @@ extension OSLog {
     }
     }
 
 
     func info(_ message: StaticString, _ args: CVarArg...) {
     func info(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.info(
                 category.logger.info(
-                    msg,
+                    {msg}(),
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,
                     line: #line
                     line: #line
@@ -72,11 +78,14 @@ extension OSLog {
     }
     }
 
 
     func `default`(_ message: StaticString, _ args: CVarArg...) {
     func `default`(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
-                    msg,
+                    {msg}(),
                     printToConsole: true,
                     printToConsole: true,
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,
@@ -87,13 +96,16 @@ extension OSLog {
     }
     }
 
 
     func error(_ message: StaticString, _ args: CVarArg...) {
     func error(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
            
            
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.warning(
                 category.logger.warning(
-                    msg,
-                    description: message.debugDescription,
+                    {msg}(),
+                    description: message.description,
                     error: nil,
                     error: nil,
                     file: #file,
                     file: #file,
                     function: #function,
                     function: #function,

+ 16 - 4
Dependencies/rileylink_ios/Common/OSLog.swift

@@ -49,7 +49,10 @@ extension OSLog {
     }
     }
 
 
     func debug(_ message: StaticString, _ args: CVarArg...) {
     func debug(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
@@ -64,7 +67,10 @@ extension OSLog {
     }
     }
 
 
     func info(_ message: StaticString, _ args: CVarArg...) {
     func info(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.info(
                 category.logger.info(
@@ -78,7 +84,10 @@ extension OSLog {
     }
     }
 
 
     func `default`(_ message: StaticString, _ args: CVarArg...) {
     func `default`(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
             loggerLock.perform {
             loggerLock.perform {
                 category.logger.debug(
                 category.logger.debug(
@@ -93,7 +102,10 @@ extension OSLog {
     }
     }
 
 
     func error(_ message: StaticString, _ args: CVarArg...) {
     func error(_ message: StaticString, _ args: CVarArg...) {
-        let msg = message.debugDescription
+        let msg_format = message.withUTF8Buffer{
+            String(decoding: $0, as: UTF8.self)
+        }
+        let msg = String(format: msg_format, args)
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
         DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
            
            
             loggerLock.perform {
             loggerLock.perform {

+ 8 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -118,7 +118,7 @@
 		3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC04273D152B00BF832C /* CalibrationService.swift */; };
 		3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC04273D152B00BF832C /* CalibrationService.swift */; };
 		3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */; };
 		3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */; };
 		3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC2D2743F9F700BF832C /* CalendarManager.swift */; };
 		3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC2D2743F9F700BF832C /* CalendarManager.swift */; };
-		386A124F271707F000DDC61C /* DexcomSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386A124E271707F000DDC61C /* DexcomSource.swift */; };
+		386A124F271707F000DDC61C /* DexcomSourceG6.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386A124E271707F000DDC61C /* DexcomSourceG6.swift */; };
 		3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3870FF4225EC13F40088248F /* BloodGlucose.swift */; };
 		3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3870FF4225EC13F40088248F /* BloodGlucose.swift */; };
 		3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F38625ED661C0013ECB5 /* Suggestion.swift */; };
 		3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F38625ED661C0013ECB5 /* Suggestion.swift */; };
 		3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39B25ED892B0013ECB5 /* TempTarget.swift */; };
 		3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39B25ED892B0013ECB5 /* TempTarget.swift */; };
@@ -307,6 +307,7 @@
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
 		CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; };
 		CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; };
 		CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = CEB434FC28B90B7C00B70274 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CECA4775298DA8310095139F /* DexcomSourceG5.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECA4774298DA8310095139F /* DexcomSourceG5.swift */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
@@ -559,7 +560,7 @@
 		3862CC04273D152B00BF832C /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		3862CC04273D152B00BF832C /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsChart.swift; sourceTree = "<group>"; };
 		3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsChart.swift; sourceTree = "<group>"; };
 		3862CC2D2743F9F700BF832C /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = "<group>"; };
 		3862CC2D2743F9F700BF832C /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = "<group>"; };
-		386A124E271707F000DDC61C /* DexcomSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSource.swift; sourceTree = "<group>"; };
+		386A124E271707F000DDC61C /* DexcomSourceG6.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSourceG6.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
 		3871F38625ED661C0013ECB5 /* Suggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = "<group>"; };
 		3871F38625ED661C0013ECB5 /* Suggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = "<group>"; };
 		3871F39B25ED892B0013ECB5 /* TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTarget.swift; sourceTree = "<group>"; };
 		3871F39B25ED892B0013ECB5 /* TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTarget.swift; sourceTree = "<group>"; };
@@ -748,6 +749,7 @@
 		CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopUIColorPalette+Default.swift"; sourceTree = "<group>"; };
+		CECA4774298DA8310095139F /* DexcomSourceG5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSourceG5.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.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>"; };
 		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>"; };
 		D295A3F870E826BE371C0BB5 /* AutotuneConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1244,11 +1246,12 @@
 				F816825D28DB441200054060 /* HeartBeatManager.swift */,
 				F816825D28DB441200054060 /* HeartBeatManager.swift */,
 				38569346270B5DFB0002C50D /* AppGroupSource.swift */,
 				38569346270B5DFB0002C50D /* AppGroupSource.swift */,
 				38569344270B5DFA0002C50D /* CGMType.swift */,
 				38569344270B5DFA0002C50D /* CGMType.swift */,
-				386A124E271707F000DDC61C /* DexcomSource.swift */,
+				386A124E271707F000DDC61C /* DexcomSourceG6.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
 				E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */,
 				E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */,
 				38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */,
 				38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */,
 				3862CC03273D150600BF832C /* Calibrations */,
 				3862CC03273D150600BF832C /* Calibrations */,
+				CECA4774298DA8310095139F /* DexcomSourceG5.swift */,
 			);
 			);
 			path = CGM;
 			path = CGM;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2224,7 +2227,7 @@
 				38FEF408273B011A00574A46 /* LibreTransmitterSource.swift in Sources */,
 				38FEF408273B011A00574A46 /* LibreTransmitterSource.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
-				386A124F271707F000DDC61C /* DexcomSource.swift in Sources */,
+				386A124F271707F000DDC61C /* DexcomSourceG6.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
@@ -2288,6 +2291,7 @@
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */,
 				CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
+				CECA4775298DA8310095139F /* DexcomSourceG5.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,

+ 20 - 13
FreeAPS/Sources/APS/APSManager.swift

@@ -72,6 +72,7 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date()
     @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date()
+    @Persisted(key: "lastStartLoopDate") private var lastStartLoopDate: Date = .distantPast
     @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
     @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
         didSet {
         didSet {
             lastLoopDateSubject.send(lastLoopDate)
             lastLoopDateSubject.send(lastLoopDate)
@@ -175,15 +176,22 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
     // Loop entry point
     // Loop entry point
     private func loop() {
     private func loop() {
+        // check the last start of looping is more the loopInterval
+        guard lastStartLoopDate.addingTimeInterval(Config.loopInterval) < Date() else {
+            debug(.apsManager, "too close to do a loop : \(lastStartLoopDate)")
+            return
+        }
+
         guard !isLooping.value else {
         guard !isLooping.value else {
-            warning(.apsManager, "Already looping, skip")
+            warning(.apsManager, "Loop already in progress. Skip recommendation.")
             return
             return
         }
         }
 
 
         debug(.apsManager, "Starting loop")
         debug(.apsManager, "Starting loop")
 
 
+        lastStartLoopDate = Date()
         var loopStatRecord = LoopStats(
         var loopStatRecord = LoopStats(
-            start: Date(),
+            start: lastStartLoopDate,
             loopStatus: "Starting"
             loopStatus: "Starting"
         )
         )
 
 
@@ -1320,17 +1328,16 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     private func loopStats(loopStatRecord: LoopStats) {
     private func loopStats(loopStatRecord: LoopStats) {
-        let file = OpenAPS.Monitor.loopStats
-
-        var uniqEvents: [LoopStats] = []
-
-        storage.transaction { storage in
-            storage.append(loopStatRecord, to: file, uniqBy: \.start)
-            uniqEvents = storage.retrieve(file, as: [LoopStats].self)?
-                .filter { $0.start.addingTimeInterval(24.hours.timeInterval) > Date() }
-                .sorted { $0.start > $1.start } ?? []
-
-            storage.save(Array(uniqEvents), as: file)
+        processQueue.async {
+            let file = OpenAPS.Monitor.loopStats
+            var uniqEvents: [LoopStats] = []
+            self.storage.transaction { storage in
+                storage.append(loopStatRecord, to: file, uniqBy: \.start)
+                uniqEvents = storage.retrieve(file, as: [LoopStats].self)?
+                    .filter { $0.start.addingTimeInterval(24.hours.timeInterval) > Date() }
+                    .sorted { $0.start > $1.start } ?? []
+                storage.save(Array(uniqEvents), as: file)
+            }
         }
         }
     }
     }
 
 

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

@@ -3,6 +3,7 @@ import Foundation
 import LibreTransmitter
 import LibreTransmitter
 
 
 struct AppGroupSource: GlucoseSource {
 struct AppGroupSource: GlucoseSource {
+    var glucoseManager: FetchGlucoseManager?
     let from: String
     let from: String
 
 
     func fetch(_ heartbeat: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
     func fetch(_ heartbeat: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
@@ -15,6 +16,10 @@ struct AppGroupSource: GlucoseSource {
         return Just(fetchLastBGs(60, sharedDefaults, heartbeat)).eraseToAnyPublisher()
         return Just(fetchLastBGs(60, sharedDefaults, heartbeat)).eraseToAnyPublisher()
     }
     }
 
 
+    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
+        fetch(nil)
+    }
+
     private func fetchLastBGs(_ count: Int, _ sharedDefaults: UserDefaults, _ heartbeat: DispatchTimer?) -> [BloodGlucose] {
     private func fetchLastBGs(_ count: Int, _ sharedDefaults: UserDefaults, _ heartbeat: DispatchTimer?) -> [BloodGlucose] {
         guard let sharedData = sharedDefaults.data(forKey: "latestReadings") else {
         guard let sharedData = sharedDefaults.data(forKey: "latestReadings") else {
             return []
             return []

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

@@ -0,0 +1,165 @@
+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: G5CGMManager?
+
+    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"))
+        cgmManager?.cgmManagerDelegate = self
+    }
+
+    var transmitterID: String {
+        cgmManager?.transmitter.ID ?? "000000"
+    }
+
+    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
+        // dexcomManager.transmitter.resumeScanning()
+        Just([]).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, tickBLE: false) {
+                        // 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 _: String?,
+        type _: LoopKit.DeviceLogEntryType,
+        message _: String,
+        completion _: ((Error?) -> Void)?
+    ) {}
+
+    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(_: CGMManager) {}
+
+    func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
+        dispatchPrecondition(condition: .onQueue(.main))
+        processCGMReadingResult(manager, readingResult: readingResult, tickBLE: true) {
+            debug(.deviceManager, "DEXCOM - Direct return")
+        }
+    }
+
+    func startDateToFilterNewData(for _: CGMManager) -> Date? {
+        dispatchPrecondition(condition: .onQueue(.main))
+        return glucoseStorage.lastGlucoseDate()
+        //  return glucoseStore.latestGlucose?.startDate
+    }
+
+    func cgmManagerDidUpdateState(_: CGMManager) {}
+
+    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,
+        tickBLE: Bool,
+        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: newGlucoseSample.syncIdentifier,
+                    sgv: value,
+                    direction: .init(trendType: newGlucoseSample.trend),
+                    date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
+                    dateString: newGlucoseSample.date,
+                    unfiltered: nil,
+                    filtered: nil,
+                    noise: nil,
+                    glucose: value,
+                    type: "sgv",
+                    transmitterID: self.transmitterID
+                )
+            }
+            if tickBLE {
+                glucoseManager?.updateGlucoseStore(newBloodGlucose: bloodGlucose)
+            } else {
+                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)"]
+    }
+}

+ 35 - 17
FreeAPS/Sources/APS/CGM/DexcomSource.swift

@@ -5,10 +5,10 @@ import LoopKit
 import LoopKitUI
 import LoopKitUI
 import ShareClient
 import ShareClient
 
 
-final class DexcomSource: GlucoseSource {
+final class DexcomSourceG6: GlucoseSource {
     private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
     private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
-    private var timer: DispatchTimer?
     private let glucoseStorage: GlucoseStorage!
     private let glucoseStorage: GlucoseStorage!
+    var glucoseManager: FetchGlucoseManager?
 
 
     var cgmManager: G6CGMManager?
     var cgmManager: G6CGMManager?
 
 
@@ -16,8 +16,9 @@ final class DexcomSource: GlucoseSource {
 
 
     private var promise: Future<[BloodGlucose], Error>.Promise?
     private var promise: Future<[BloodGlucose], Error>.Promise?
 
 
-    init(glucoseStorage: GlucoseStorage) {
+    init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
         self.glucoseStorage = glucoseStorage
         self.glucoseStorage = glucoseStorage
+        self.glucoseManager = glucoseManager
         cgmManager = G6CGMManager
         cgmManager = G6CGMManager
             .init(state: TransmitterManagerState(transmitterID: UserDefaults.standard.dexcomTransmitterID ?? "000000"))
             .init(state: TransmitterManagerState(transmitterID: UserDefaults.standard.dexcomTransmitterID ?? "000000"))
         cgmManager?.cgmManagerDelegate = self
         cgmManager?.cgmManagerDelegate = self
@@ -27,13 +28,22 @@ final class DexcomSource: GlucoseSource {
         cgmManager?.transmitter.ID ?? "000000"
         cgmManager?.transmitter.ID ?? "000000"
     }
     }
 
 
-    func fetch(_ heartbeat: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        // dexcomManager.transmitter.resumeScanning()
-        timer = heartbeat
-        return Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
+    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
+        fetchIfNeeded()
+    }
+
+    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, tickBLE: false) {
+                        // nothing to do
+                    }
+                }
+            }
         }
         }
-        .timeout(90, scheduler: processQueue, options: nil, customError: nil)
+        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
         .replaceError(with: [])
         .replaceError(with: [])
         .replaceEmpty(with: [])
         .replaceEmpty(with: [])
         .eraseToAnyPublisher()
         .eraseToAnyPublisher()
@@ -44,7 +54,7 @@ final class DexcomSource: GlucoseSource {
     }
     }
 }
 }
 
 
-extension DexcomSource: CGMManagerDelegate {
+extension DexcomSourceG6: CGMManagerDelegate {
     func deviceManager(
     func deviceManager(
         _: LoopKit.DeviceManager,
         _: LoopKit.DeviceManager,
         logEventForDeviceIdentifier _: String?,
         logEventForDeviceIdentifier _: String?,
@@ -75,9 +85,8 @@ extension DexcomSource: CGMManagerDelegate {
 
 
     func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
     func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
         dispatchPrecondition(condition: .onQueue(.main))
         dispatchPrecondition(condition: .onQueue(.main))
-        processCGMReadingResult(manager, readingResult: readingResult) {
-//            warning(.deviceManager, "DEXCOM - Force the fire of the dispatch timer")
-//            self.timer?.fire()
+        processCGMReadingResult(manager, readingResult: readingResult, tickBLE: true) {
+            debug(.deviceManager, "DEXCOM - Direct return")
         }
         }
     }
     }
 
 
@@ -102,8 +111,13 @@ extension DexcomSource: CGMManagerDelegate {
         }
         }
     }
     }
 
 
-    private func processCGMReadingResult(_: CGMManager, readingResult: CGMReadingResult, completion: @escaping () -> Void) {
-        warning(.deviceManager, "DEXCOM - Process CGM Reading Result launched")
+    private func processCGMReadingResult(
+        _: CGMManager,
+        readingResult: CGMReadingResult,
+        tickBLE: Bool,
+        completion: @escaping () -> Void
+    ) {
+        debug(.deviceManager, "DEXCOM - Process CGM Reading Result launched")
         switch readingResult {
         switch readingResult {
         case let .newData(values):
         case let .newData(values):
             let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
             let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
@@ -123,7 +137,11 @@ extension DexcomSource: CGMManagerDelegate {
                     transmitterID: self.transmitterID
                     transmitterID: self.transmitterID
                 )
                 )
             }
             }
-            promise?(.success(bloodGlucose))
+            if tickBLE {
+                glucoseManager?.updateGlucoseStore(newBloodGlucose: bloodGlucose)
+            } else {
+                promise?(.success(bloodGlucose))
+            }
             completion()
             completion()
         case .unreliableData:
         case .unreliableData:
             // loopManager.receivedUnreliableCGMReading()
             // loopManager.receivedUnreliableCGMReading()
@@ -139,7 +157,7 @@ extension DexcomSource: CGMManagerDelegate {
     }
     }
 }
 }
 
 
-extension DexcomSource {
+extension DexcomSourceG6 {
     func sourceInfo() -> [String: Any]? {
     func sourceInfo() -> [String: Any]? {
         [GlucoseSourceKey.description.rawValue: "Dexcom tramsmitter ID: \(transmitterID)"]
         [GlucoseSourceKey.description.rawValue: "Dexcom tramsmitter ID: \(transmitterID)"]
     }
     }

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

@@ -26,6 +26,8 @@ import Foundation
 // MARK: - Glucose simulator
 // MARK: - Glucose simulator
 
 
 final class GlucoseSimulatorSource: GlucoseSource {
 final class GlucoseSimulatorSource: GlucoseSource {
+    var glucoseManager: FetchGlucoseManager?
+
     private enum Config {
     private enum Config {
         // min time period to publish data
         // min time period to publish data
         static let workInterval: TimeInterval = 300
         static let workInterval: TimeInterval = 300
@@ -81,6 +83,10 @@ final class GlucoseSimulatorSource: GlucoseSource {
 
 
         return Just(glucoses).eraseToAnyPublisher()
         return Just(glucoses).eraseToAnyPublisher()
     }
     }
+
+    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
+        fetch(nil)
+    }
 }
 }
 
 
 // MARK: - Glucose generator
 // MARK: - Glucose generator
@@ -164,7 +170,7 @@ class IntelligentGenerator: BloodGlucoseGenerator {
     }
     }
 
 
     private func getDirection(fromGlucose from: Int, toGlucose to: Int) -> BloodGlucose.Direction {
     private func getDirection(fromGlucose from: Int, toGlucose to: Int) -> BloodGlucose.Direction {
-        BloodGlucose.Direction(trend: to - from)
+        BloodGlucose.Direction(trend: Int(to - from))
     }
     }
 
 
     private func generateNewTrend() {
     private func generateNewTrend() {

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

@@ -6,6 +6,8 @@ protocol SourceInfoProvider {
 
 
 protocol GlucoseSource: SourceInfoProvider {
 protocol GlucoseSource: SourceInfoProvider {
     func fetch(_ heartbeat: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never>
     func fetch(_ heartbeat: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never>
+    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never>
+    var glucoseManager: FetchGlucoseManager? { get set }
 }
 }
 
 
 extension GlucoseSource {
 extension GlucoseSource {

+ 15 - 7
FreeAPS/Sources/APS/CGM/LibreTransmitterSource.swift

@@ -15,6 +15,8 @@ final class BaseLibreTransmitterSource: LibreTransmitterSource, Injectable {
 
 
     private var promise: Future<[BloodGlucose], Error>.Promise?
     private var promise: Future<[BloodGlucose], Error>.Promise?
 
 
+    var glucoseManager: FetchGlucoseManager?
+
     var manager: LibreTransmitterManager? {
     var manager: LibreTransmitterManager? {
         didSet {
         didSet {
             configured = manager != nil
             configured = manager != nil
@@ -34,13 +36,18 @@ final class BaseLibreTransmitterSource: LibreTransmitterSource, Injectable {
     }
     }
 
 
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
     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()
+        Just([]).eraseToAnyPublisher()
+//        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> {
+        fetch(nil)
     }
     }
 
 
     func sourceInfo() -> [String: Any]? {
     func sourceInfo() -> [String: Any]? {
@@ -80,6 +87,7 @@ extension BaseLibreTransmitterSource: LibreTransmitterManagerDelegate {
                 )
                 )
             }
             }
             NSLog("Debug Libre \(glucose)")
             NSLog("Debug Libre \(glucose)")
+            glucoseManager?.updateGlucoseStore(newBloodGlucose: glucose)
             promise?(.success(glucose))
             promise?(.success(glucose))
 
 
         case let .failure(error):
         case let .failure(error):

+ 33 - 14
FreeAPS/Sources/APS/CGM/dexcomSourceG7.swift

@@ -7,26 +7,37 @@ import LoopKitUI
 
 
 final class DexcomSourceG7: GlucoseSource {
 final class DexcomSourceG7: GlucoseSource {
     private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
     private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
-    private var timer: DispatchTimer?
     private var glucoseStorage: GlucoseStorage!
     private var glucoseStorage: GlucoseStorage!
+    var glucoseManager: FetchGlucoseManager?
 
 
-    var cgmManager: G7CGMManager
+    var cgmManager: G7CGMManager?
 
 
     var cgmHasValidSensorSession: Bool = false
     var cgmHasValidSensorSession: Bool = false
 
 
     private var promise: Future<[BloodGlucose], Error>.Promise?
     private var promise: Future<[BloodGlucose], Error>.Promise?
 
 
-    init(glucoseStorage: GlucoseStorage) {
-        cgmManager = G7CGMManager()
-        cgmManager.cgmManagerDelegate = self
+    init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
         self.glucoseStorage = glucoseStorage
         self.glucoseStorage = glucoseStorage
+        self.glucoseManager = glucoseManager
+        cgmManager = G7CGMManager()
+        cgmManager?.cgmManagerDelegate = self
     }
     }
 
 
-    func fetch(_ heartbeat: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
+    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         // dexcomManager.transmitter.resumeScanning()
         // dexcomManager.transmitter.resumeScanning()
-        timer = heartbeat
-        return Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
+        Just([]).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, tickBLE: false) {
+                        // nothing to do
+                    }
+                }
+            }
         }
         }
         .timeout(60, scheduler: processQueue, options: nil, customError: nil)
         .timeout(60, scheduler: processQueue, options: nil, customError: nil)
         .replaceError(with: [])
         .replaceError(with: [])
@@ -70,15 +81,14 @@ extension DexcomSourceG7: CGMManagerDelegate {
 
 
     func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
     func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
         dispatchPrecondition(condition: .onQueue(.main))
         dispatchPrecondition(condition: .onQueue(.main))
-        processCGMReadingResult(manager, readingResult: readingResult) {
-            // self.checkPumpDataAndLoop()
+        processCGMReadingResult(manager, readingResult: readingResult, tickBLE: true) {
+            // nothing to do
         }
         }
     }
     }
 
 
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
         dispatchPrecondition(condition: .onQueue(.main))
         dispatchPrecondition(condition: .onQueue(.main))
         return glucoseStorage.lastGlucoseDate()
         return glucoseStorage.lastGlucoseDate()
-        //  return glucoseStore.latestGlucose?.startDate
     }
     }
 
 
     func cgmManagerDidUpdateState(_: CGMManager) {}
     func cgmManagerDidUpdateState(_: CGMManager) {}
@@ -96,7 +106,12 @@ extension DexcomSourceG7: CGMManagerDelegate {
         }
         }
     }
     }
 
 
-    private func processCGMReadingResult(_: CGMManager, readingResult: CGMReadingResult, completion: @escaping () -> Void) {
+    private func processCGMReadingResult(
+        _: CGMManager,
+        readingResult: CGMReadingResult,
+        tickBLE: Bool,
+        completion: @escaping () -> Void
+    ) {
         warning(.deviceManager, "DEXCOMG7 - Process CGM Reading Result launched")
         warning(.deviceManager, "DEXCOMG7 - Process CGM Reading Result launched")
         switch readingResult {
         switch readingResult {
         case let .newData(values):
         case let .newData(values):
@@ -116,7 +131,11 @@ extension DexcomSourceG7: CGMManagerDelegate {
                     type: "sgv"
                     type: "sgv"
                 )
                 )
             }
             }
-            promise?(.success(bloodGlucose))
+            if tickBLE {
+                glucoseManager?.updateGlucoseStore(newBloodGlucose: bloodGlucose)
+            } else {
+                promise?(.success(bloodGlucose))
+            }
             completion()
             completion()
         case .unreliableData:
         case .unreliableData:
             // loopManager.receivedUnreliableCGMReading()
             // loopManager.receivedUnreliableCGMReading()

+ 42 - 16
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -23,6 +23,7 @@ protocol DeviceDataManager: GlucoseSource {
     var errorSubject: PassthroughSubject<Error, Never> { get }
     var errorSubject: PassthroughSubject<Error, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
+    var requireCGMRefresh: PassthroughSubject<Date, Never> { get }
     func heartbeat(date: Date)
     func heartbeat(date: Date)
     func createBolusProgressReporter() -> DoseProgressReporter?
     func createBolusProgressReporter() -> DoseProgressReporter?
     var alertHistoryStorage: AlertHistoryStorage! { get }
     var alertHistoryStorage: AlertHistoryStorage! { get }
@@ -62,11 +63,15 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     @SyncAccess(lock: accessLock) @Persisted(key: "BaseDeviceDataManager.lastHeartBeatTime") var lastHeartBeatTime: Date =
     @SyncAccess(lock: accessLock) @Persisted(key: "BaseDeviceDataManager.lastHeartBeatTime") var lastHeartBeatTime: Date =
         .distantPast
         .distantPast
 
 
+    // to do at true if you would like to use pump heartbeat
+    let heartbeatBypump: Bool = false
+
     let recommendsLoop = PassthroughSubject<Void, Never>()
     let recommendsLoop = PassthroughSubject<Void, Never>()
     let bolusTrigger = PassthroughSubject<Bool, Never>()
     let bolusTrigger = PassthroughSubject<Bool, Never>()
     let errorSubject = PassthroughSubject<Error, Never>()
     let errorSubject = PassthroughSubject<Error, Never>()
     let pumpNewStatus = PassthroughSubject<Void, Never>()
     let pumpNewStatus = PassthroughSubject<Void, Never>()
     let manualTempBasal = PassthroughSubject<Bool, Never>()
     let manualTempBasal = PassthroughSubject<Bool, Never>()
+    let requireCGMRefresh = PassthroughSubject<Date, Never>()
     private let router = FreeAPSApp.resolver.resolve(Router.self)!
     private let router = FreeAPSApp.resolver.resolve(Router.self)!
     @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
     @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
     private var pumpUpdatePromise: Future<Bool, Never>.Promise?
     private var pumpUpdatePromise: Future<Bool, Never>.Promise?
@@ -86,6 +91,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         pumpExpiresAtDate.send(nil)
                         pumpExpiresAtDate.send(nil)
                         return
                         return
                     }
                     }
+                    pumpManager.setMustProvideBLEHeartbeat(heartbeatBypump)
                     pumpExpiresAtDate.send(endTime)
                     pumpExpiresAtDate.send(endTime)
                 }
                 }
                 if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
                 if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
@@ -93,6 +99,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         pumpExpiresAtDate.send(nil)
                         pumpExpiresAtDate.send(nil)
                         return
                         return
                     }
                     }
+                    pumpManager.setMustProvideBLEHeartbeat(heartbeatBypump)
                     pumpExpiresAtDate.send(endTime)
                     pumpExpiresAtDate.send(endTime)
                 }
                 }
             } else {
             } else {
@@ -155,19 +162,25 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         }
         }
 
 
         debug(.deviceManager, "Start updating the pump data")
         debug(.deviceManager, "Start updating the pump data")
-        pumpUpdateCancellable = Future<Bool, Never> { [unowned self] promise in
-            pumpUpdatePromise = promise
-            debug(.deviceManager, "Waiting for pump update and loop recommendation")
-            processQueue.safeSync {
-                pumpManager.ensureCurrentPumpData { _ in
-                    debug(.deviceManager, "Pump data updated.")
-                }
-            }
+
+        pumpManager.ensureCurrentPumpData { _ in
+            debug(.deviceManager, "Pump data updated.")
+            self.updateUpdateFinished(true)
         }
         }
-        .timeout(30, scheduler: processQueue)
-        .replaceError(with: false)
-        .replaceEmpty(with: false)
-        .sink(receiveValue: updateUpdateFinished)
+
+//        pumpUpdateCancellable = Future<Bool, Never> { [unowned self] promise in
+//            pumpUpdatePromise = promise
+//            debug(.deviceManager, "Waiting for pump update and loop recommendation")
+//            processQueue.safeSync {
+//                pumpManager.ensureCurrentPumpData { _ in
+//                    debug(.deviceManager, "Pump data updated.")
+//                }
+//            }
+//        }
+//        .timeout(30, scheduler: processQueue)
+//        .replaceError(with: false)
+//        .replaceEmpty(with: false)
+//        .sink(receiveValue: updateUpdateFinished)
     }
     }
 
 
     private func updateUpdateFinished(_ recommendsLoop: Bool) {
     private func updateUpdateFinished(_ recommendsLoop: Bool) {
@@ -176,10 +189,12 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         if !recommendsLoop {
         if !recommendsLoop {
             warning(.deviceManager, "Loop recommendation time out or got error. Trying to loop right now.")
             warning(.deviceManager, "Loop recommendation time out or got error. Trying to loop right now.")
         }
         }
-        guard !loopInProgress else {
-            warning(.deviceManager, "Loop already in progress. Skip recommendation.")
-            return
-        }
+
+        // directly in loop() function
+//        guard !loopInProgress else {
+//            warning(.deviceManager, "Loop already in progress. Skip recommendation.")
+//            return
+//        }
         self.recommendsLoop.send()
         self.recommendsLoop.send()
     }
     }
 
 
@@ -205,6 +220,12 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 
 
     @Persisted(key: "BaseDeviceDataManager.lastFetchGlucoseDate") private var lastFetchGlucoseDate: Date = .distantPast
     @Persisted(key: "BaseDeviceDataManager.lastFetchGlucoseDate") private var lastFetchGlucoseDate: Date = .distantPast
 
 
+    var glucoseManager: FetchGlucoseManager?
+
+    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
+        fetch(nil)
+    }
+
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         guard let medtronic = pumpManager as? MinimedPumpManager else {
         guard let medtronic = pumpManager as? MinimedPumpManager else {
             warning(.deviceManager, "Fetch minilink glucose failed: Pump is not Medtronic")
             warning(.deviceManager, "Fetch minilink glucose failed: Pump is not Medtronic")
@@ -271,6 +292,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     }
     }
 }
 }
 
 
+// MARK: - PumpManagerDelegate
+
 extension BaseDeviceDataManager: PumpManagerDelegate {
 extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManagerPumpWasReplaced(_: PumpManager) {
     func pumpManagerPumpWasReplaced(_: PumpManager) {
         debug(.deviceManager, "pumpManagerPumpWasReplaced")
         debug(.deviceManager, "pumpManagerPumpWasReplaced")
@@ -295,6 +318,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
 
     func pumpManagerBLEHeartbeatDidFire(_: PumpManager) {
     func pumpManagerBLEHeartbeatDidFire(_: PumpManager) {
         debug(.deviceManager, "Pump Heartbeat: do nothing. Pump connection is OK")
         debug(.deviceManager, "Pump Heartbeat: do nothing. Pump connection is OK")
+        requireCGMRefresh.send(Date())
     }
     }
 
 
     func pumpManagerMustProvideBLEHeartbeat(_: PumpManager) -> Bool {
     func pumpManagerMustProvideBLEHeartbeat(_: PumpManager) -> Bool {
@@ -542,6 +566,8 @@ extension BaseDeviceDataManager: DeviceManagerDelegate {
     }
     }
 }
 }
 
 
+// MARK: - CGMManagerDelegate
+
 extension BaseDeviceDataManager: CGMManagerDelegate {
 extension BaseDeviceDataManager: CGMManagerDelegate {
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
         glucoseStorage.syncDate().addingTimeInterval(-10.minutes.timeInterval) // additional time to calculate directions
         glucoseStorage.syncDate().addingTimeInterval(-10.minutes.timeInterval) // additional time to calculate directions

+ 92 - 28
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -3,7 +3,10 @@ import Foundation
 import SwiftDate
 import SwiftDate
 import Swinject
 import Swinject
 
 
-protocol FetchGlucoseManager: SourceInfoProvider {}
+protocol FetchGlucoseManager: SourceInfoProvider {
+    func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
+    func refreshCGM()
+}
 
 
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
@@ -18,14 +21,23 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private var lifetime = Lifetime()
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
 
 
-    private lazy var dexcomSource = DexcomSource(glucoseStorage: glucoseStorage)
-    private lazy var dexcomSourceG7 = DexcomSourceG7(glucoseStorage: glucoseStorage)
+    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 simulatorSource = GlucoseSimulatorSource()
     private lazy var simulatorSource = GlucoseSimulatorSource()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         updateGlucoseSource()
         updateGlucoseSource()
         subscribe()
         subscribe()
+
+        /// listen if require CGM update
+        deviceDataManager.requireCGMRefresh
+            .receive(on: processQueue)
+            .sink { _ in
+                self.refreshCGM()
+            }
+            .store(in: &lifetime)
     }
     }
 
 
     var glucoseSource: GlucoseSource!
     var glucoseSource: GlucoseSource!
@@ -34,9 +46,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         switch settingsManager.settings.cgm {
         switch settingsManager.settings.cgm {
         case .xdrip:
         case .xdrip:
             glucoseSource = AppGroupSource(from: "xDrip")
             glucoseSource = AppGroupSource(from: "xDrip")
-        case .dexcomG5,
-             .dexcomG6:
-            glucoseSource = dexcomSource
+        case .dexcomG5:
+            glucoseSource = dexcomSourceG5
+        case .dexcomG6:
+            glucoseSource = dexcomSourceG6
         case .dexcomG7:
         case .dexcomG7:
             glucoseSource = dexcomSourceG7
             glucoseSource = dexcomSourceG7
         case .nightscout:
         case .nightscout:
@@ -53,15 +66,77 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
 
         if settingsManager.settings.cgm != .libreTransmitter {
         if settingsManager.settings.cgm != .libreTransmitter {
             libreTransmitter.manager = nil
             libreTransmitter.manager = nil
+        } else {
+            libreTransmitter.glucoseManager = self
         }
         }
     }
     }
 
 
+    /// function called when a callback is fired by CGM BLE
+    public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
+        let syncDate = glucoseStorage.syncDate()
+        debug(.deviceManager, "CGM BLE FETCHGLUCOSE  : SyncDate is \(syncDate)")
+        glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
+    }
+
+    /// 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()
+        Publishers.CombineLatest3(
+            Just(glucoseStorage.syncDate()),
+            healthKitManager.fetch(nil),
+            glucoseSource.fetchIfNeeded()
+        )
+        .eraseToAnyPublisher()
+        .receive(on: processQueue)
+        .sink { syncDate, glucoseFromHealth, glucose in
+            debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
+            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
+        }
+        .store(in: &lifetime)
+    }
+
+    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
+        let allGlucose = glucose + glucoseFromHealth
+        var filteredByDate: [BloodGlucose] = []
+        var filtered: [BloodGlucose] = []
+
+        if allGlucose.isNotEmpty {
+            filteredByDate = allGlucose.filter { $0.dateString > syncDate }
+            filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
+            if filtered.isNotEmpty {
+                debug(.nightscout, "New glucose found")
+                glucoseStorage.storeGlucose(filtered)
+            }
+        }
+
+        if filtered.isEmpty {
+            let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
+            guard lastGlucoseDate >= Date().addingTimeInterval(Config.eхpirationInterval) else {
+                debug(.nightscout, "Glucose is too old - \(lastGlucoseDate)")
+                return
+            }
+        }
+
+        apsManager.heartbeat(date: Date())
+
+        // no need to go next step if no new data
+        guard filteredByDate.isNotEmpty else {
+            return
+        }
+
+        nightscoutManager.uploadGlucose()
+        let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
+        guard glucoseForHealth.isNotEmpty else { return }
+        healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
+    }
+
+    /// The function used to start the timer sync - Function of the variable defined in config
     private func subscribe() {
     private func subscribe() {
         timer.publisher
         timer.publisher
             .receive(on: processQueue)
             .receive(on: processQueue)
             .flatMap { date -> AnyPublisher<(Date, Date, [BloodGlucose], [BloodGlucose]), Never> in
             .flatMap { date -> AnyPublisher<(Date, Date, [BloodGlucose], [BloodGlucose]), Never> in
                 debug(.nightscout, "FetchGlucoseManager heartbeat")
                 debug(.nightscout, "FetchGlucoseManager heartbeat")
-                // debug(.nightscout, "Start fetching glucose")
                 self.updateGlucoseSource()
                 self.updateGlucoseSource()
                 return Publishers.CombineLatest4(
                 return Publishers.CombineLatest4(
                     Just(date),
                     Just(date),
@@ -71,25 +146,9 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                 )
                 )
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
             }
             }
-            .sink { date, syncDate, glucose, glucoseFromHealth in
+            .sink { _, syncDate, glucose, glucoseFromHealth in
                 debug(.nightscout, "FETCHGLUCOSE : SyncDate is \(syncDate)")
                 debug(.nightscout, "FETCHGLUCOSE : SyncDate is \(syncDate)")
-                let allGlucose = glucose + glucoseFromHealth
-                guard allGlucose.isNotEmpty else { return }
-
-                // Because of Spike dosn't respect a date query
-                let filteredByDate = allGlucose.filter { $0.dateString > syncDate }
-                let filtered = self.glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
-
-                guard filtered.isNotEmpty else { return }
-                debug(.nightscout, "New glucose found")
-
-                self.glucoseStorage.storeGlucose(filtered)
-                self.apsManager.heartbeat(date: date)
-                self.nightscoutManager.uploadGlucose()
-                let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
-
-                guard glucoseForHealth.isNotEmpty else { return }
-                self.healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
+                self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
             }
             }
             .store(in: &lifetime)
             .store(in: &lifetime)
         timer.fire()
         timer.fire()
@@ -99,9 +158,14 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             .publisher(for: \.dexcomTransmitterID)
             .publisher(for: \.dexcomTransmitterID)
             .removeDuplicates()
             .removeDuplicates()
             .sink { id in
             .sink { id in
-                guard [.dexcomG5, .dexcomG6].contains(self.settingsManager.settings.cgm) else { return }
-                if id != self.dexcomSource.transmitterID {
-                    self.dexcomSource = DexcomSource(glucoseStorage: self.glucoseStorage)
+                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)
             .store(in: &lifetime)

+ 26 - 20
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -34,6 +34,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     func storeGlucose(_ glucose: [BloodGlucose]) {
     func storeGlucose(_ glucose: [BloodGlucose]) {
         processQueue.sync {
         processQueue.sync {
+            debug(.deviceManager, "start storage glucose")
             let file = OpenAPS.Monitor.glucose
             let file = OpenAPS.Monitor.glucose
             self.storage.transaction { storage in
             self.storage.transaction { storage in
                 storage.append(glucose, to: file, uniqBy: \.dateString)
                 storage.append(glucose, to: file, uniqBy: \.dateString)
@@ -48,32 +49,15 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         $0.glucoseDidUpdate(glucose.reversed())
                         $0.glucoseDidUpdate(glucose.reversed())
                     }
                     }
                 }
                 }
-
-                // Save to glucoseForStats also.
-                var bg_ = 0
-                var bgDate = Date()
-
-                if glucose.isNotEmpty {
-                    bg_ = glucose[0].glucose ?? 0
-                    bgDate = glucose[0].dateString
-                }
-                if bg_ != 0 {
-                    let dataForStats = GlucoseDataForStats(date: bgDate, glucose: bg_)
-                    storage.append(dataForStats, to: OpenAPS.Monitor.glucose_data, uniqBy: \.date)
-                    let uniqEvents_1 = storage.retrieve(OpenAPS.Monitor.glucose_data, as: [GlucoseDataForStats].self)?
-                        .filter { $0.date.addingTimeInterval(90.days.timeInterval) > Date() }
-                        .sorted { $0.date > $1.date } ?? []
-                    let dataForStats_ = Array(uniqEvents_1)
-                    storage.save(dataForStats_, as: OpenAPS.Monitor.glucose_data)
-                }
             }
             }
 
 
+            debug(.deviceManager, "start storage cgmState")
             self.storage.transaction { storage in
             self.storage.transaction { storage in
                 let file = OpenAPS.Monitor.cgmState
                 let file = OpenAPS.Monitor.cgmState
                 var treatments = storage.retrieve(file, as: [NigtscoutTreatment].self) ?? []
                 var treatments = storage.retrieve(file, as: [NigtscoutTreatment].self) ?? []
                 var updated = false
                 var updated = false
                 for x in glucose {
                 for x in glucose {
-                    NSLog("storeGlucose \(x)")
+                    debug(.deviceManager, "storeGlucose \(x)")
                     guard let sessionStartDate = x.sessionStartDate else {
                     guard let sessionStartDate = x.sessionStartDate else {
                         continue
                         continue
                     }
                     }
@@ -108,7 +92,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         targetTop: nil,
                         targetTop: nil,
                         targetBottom: nil
                         targetBottom: nil
                     )
                     )
-                    NSLog("CGM sensor change \(treatment)")
+                    debug(.deviceManager, "CGM sensor change \(treatment)")
                     treatments.append(treatment)
                     treatments.append(treatment)
                     updated = true
                     updated = true
                 }
                 }
@@ -122,6 +106,28 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 }
                 }
             }
             }
         }
         }
+        processQueue.async {
+            self.storage.transaction { storage in
+                debug(.deviceManager, "start storage glucose stats")
+                // Save to glucoseForStats also.
+                var bg_ = 0
+                var bgDate = Date()
+
+                if glucose.isNotEmpty {
+                    bg_ = glucose[0].glucose ?? 0
+                    bgDate = glucose[0].dateString
+                }
+                if bg_ != 0 {
+                    let dataForStats = GlucoseDataForStats(date: bgDate, glucose: bg_)
+                    storage.append(dataForStats, to: OpenAPS.Monitor.glucose_data, uniqBy: \.date)
+                    let uniqEvents_1 = storage.retrieve(OpenAPS.Monitor.glucose_data, as: [GlucoseDataForStats].self)?
+                        .filter { $0.date.addingTimeInterval(90.days.timeInterval) > Date() }
+                        .sorted { $0.date > $1.date } ?? []
+                    let dataForStats_ = Array(uniqEvents_1)
+                    storage.save(dataForStats_, as: OpenAPS.Monitor.glucose_data)
+                }
+            }
+        }
     }
     }
 
 
     func removeGlucose(ids: [String]) {
     func removeGlucose(ids: [String]) {

+ 1 - 1
FreeAPS/Sources/Config/Config.swift

@@ -4,6 +4,6 @@ import SwiftDate
 enum Config {
 enum Config {
     static let treatWarningsAsErrors = true
     static let treatWarningsAsErrors = true
     static let withSignPosts = false
     static let withSignPosts = false
-    static let loopInterval = 3.minutes.timeInterval
+    static let loopInterval = 4.minutes.timeInterval
     static let eхpirationInterval = 10.minutes.timeInterval
     static let eхpirationInterval = 10.minutes.timeInterval
 }
 }

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

@@ -287,6 +287,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         )
         )
     }
     }
 
 
+    // MARK: - GlucoseSource
+
+    var glucoseManager: FetchGlucoseManager?
+
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         Future { [weak self] promise in
         Future { [weak self] promise in
             guard let self = self else {
             guard let self = self else {
@@ -322,6 +326,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         .eraseToAnyPublisher()
         .eraseToAnyPublisher()
     }
     }
 
 
+    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
+        fetch(nil)
+    }
+
     func deleteGlucise(syncID: String) {
     func deleteGlucise(syncID: String) {
         guard settingsManager.settings.useAppleHealth,
         guard settingsManager.settings.useAppleHealth,
               let sampleType = Config.healthBGObject,
               let sampleType = Config.healthBGObject,

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

@@ -125,10 +125,18 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .eraseToAnyPublisher()
             .eraseToAnyPublisher()
     }
     }
 
 
+    // MARK: - GlucoseSource
+
+    var glucoseManager: FetchGlucoseManager?
+
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         fetchGlucose(since: glucoseStorage.syncDate())
         fetchGlucose(since: glucoseStorage.syncDate())
     }
     }
 
 
+    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
+        fetch(nil)
+    }
+
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> {
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never> {
         guard let nightscout = nightscoutAPI, isNetworkReachable else {
         guard let nightscout = nightscoutAPI, isNetworkReachable else {
             return Just([]).eraseToAnyPublisher()
             return Just([]).eraseToAnyPublisher()