Переглянути джерело

Release/0.1.11 (#20)


* fix typo

* Loop error in popup

* Hide NS url in logs

* Write logs into a file

* Bolus progress

* Bump version
Ivan 5 роки тому
батько
коміт
82e235f24b

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -198,6 +198,7 @@
 		38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A2C25F52DC400C0CED0 /* NSLocking+Extensions.swift */; };
 		38E98A3025F52FF700C0CED0 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A2F25F52FF700C0CED0 /* Config.swift */; };
 		38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A3625F5509500C0CED0 /* String+Extensions.swift */; };
+		38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */; };
 		38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F37827261260DC009DB701 /* Color+Extensions.swift */; };
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
@@ -479,6 +480,7 @@
 		38E98A2C25F52DC400C0CED0 /* NSLocking+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLocking+Extensions.swift"; sourceTree = "<group>"; };
 		38E98A2F25F52FF700C0CED0 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
 		38E98A3625F5509500C0CED0 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
+		38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleLogReporter.swift; sourceTree = "<group>"; };
 		38F37827261260DC009DB701 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
 		38F3783A2613555C009DB701 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
 		38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetsStorage.swift; sourceTree = "<group>"; };
@@ -1188,6 +1190,7 @@
 			children = (
 				38E98A1E25F52C9300C0CED0 /* IssueReporter.swift */,
 				38E98A2025F52C9300C0CED0 /* CollectionIssueReporter.swift */,
+				38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */,
 			);
 			path = IssueReporter;
 			sourceTree = "<group>";
@@ -1702,6 +1705,7 @@
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
+				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				3811DE3325C9D49500A708ED /* HomeBuilder.swift in Sources */,

+ 1 - 1
FreeAPS/Resources/Config.xcconfig

@@ -1 +1 @@
-BUILD_VERSION = 0.1.10
+BUILD_VERSION = 0.1.11

+ 85 - 11
FreeAPS/Sources/APS/APSManager.swift

@@ -13,12 +13,35 @@ protocol APSManager {
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var isLooping: CurrentValueSubject<Bool, Never> { get }
-    var lastLoopDate: PassthroughSubject<Date, Never> { get }
+    var lastLoopDate: Date { get }
+    var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
+    var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     func enactTempBasal(rate: Double, duration: TimeInterval)
     func makeProfiles() -> AnyPublisher<Bool, Never>
     func determineBasal() -> AnyPublisher<Bool, Never>
     func roundBolus(amount: Decimal) -> Decimal
+    var lastError: CurrentValueSubject<Error?, Never> { get }
+}
+
+enum APSError: LocalizedError {
+    case pumpError(Error)
+    case invalidPumpState(message: String)
+    case glucoseError(message: String)
+    case apsError(message: String)
+
+    var errorDescription: String? {
+        switch self {
+        case let .pumpError(error):
+            return "Pump error: \(error.localizedDescription)"
+        case let .invalidPumpState(message):
+            return "Invalid Pump State: \(message)"
+        case let .glucoseError(message):
+            return "Invalid glucose: \(message)"
+        case let .apsError(message):
+            return "APS error: \(message)"
+        }
+    }
 }
 
 final class BaseAPSManager: APSManager, Injectable {
@@ -34,6 +57,11 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate: Date = .distantPast
+    @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
+        didSet {
+            lastLoopDateSubject.send(lastLoopDate)
+        }
+    }
 
     private var openAPS: OpenAPS!
 
@@ -45,7 +73,10 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     let isLooping = CurrentValueSubject<Bool, Never>(false)
-    let lastLoopDate = PassthroughSubject<Date, Never>()
+    let lastLoopDateSubject = PassthroughSubject<Date, Never>()
+    let lastError = CurrentValueSubject<Error?, Never>(nil)
+
+    let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
 
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
         deviceDataManager.pumpDisplayState
@@ -72,11 +103,20 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private func subscribe() {
         deviceDataManager.recommendsLoop
+            .receive(on: processQueue)
             .sink { [weak self] in
                 self?.fetchAndLoop()
             }
             .store(in: &lifetime)
         pumpManager?.addStatusObserver(self, queue: processQueue)
+
+        deviceDataManager.errorSubject
+            .receive(on: processQueue)
+            .map { APSError.pumpError($0) }
+            .sink {
+                self.processError($0)
+            }
+            .store(in: &lifetime)
     }
 
     func heartbeat(date: Date, force: Bool) {
@@ -116,7 +156,7 @@ final class BaseAPSManager: APSManager, Injectable {
                         self.enactSuggested()
                     } else {
                         self.isLooping.send(false)
-                        self.lastLoopDate.send(Date())
+                        self.lastLoopDate = Date()
                     }
                 } else {
                     self.isLooping.send(false)
@@ -127,18 +167,21 @@ final class BaseAPSManager: APSManager, Injectable {
     private func verifyStatus() -> Bool {
         guard let pump = pumpManager else {
             debug(.apsManager, "Pump is not set")
+            processError(APSError.invalidPumpState(message: "Pump is not set"))
             return false
         }
         let status = pump.status.pumpStatus
 
         guard !status.bolusing, !status.suspended else {
             debug(.apsManager, "Pump is bolusing or suspended")
+            processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
             return false
         }
 
         let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
         guard reservoir > 0 else {
             debug(.apsManager, "Reservoir is empty")
+            processError(APSError.invalidPumpState(message: "Reservoir is empty"))
             return false
         }
 
@@ -160,12 +203,14 @@ final class BaseAPSManager: APSManager, Injectable {
     func determineBasal() -> AnyPublisher<Bool, Never> {
         guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
             debug(.apsManager, "Not enough glucose data")
+            processError(APSError.glucoseError(message: "Not enough glucose data"))
             return Just(false).eraseToAnyPublisher()
         }
 
         let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
         guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
             debug(.apsManager, "Glucose data is stale")
+            processError(APSError.glucoseError(message: "Glucose data is stale"))
             return Just(false).eraseToAnyPublisher()
         }
 
@@ -224,21 +269,28 @@ final class BaseAPSManager: APSManager, Injectable {
         return Decimal(pump.roundToSupportedBolusVolume(units: Double(amount)))
     }
 
+    private var bolusReporter: DoseProgressReporter?
+
     func enactBolus(amount: Double, isSMB: Bool) {
         guard let pump = pumpManager, verifyStatus() else { return }
 
         let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
-        pump.enactBolus(units: roundedAmout, automatic: false) { result in
-            switch result {
-            case .success:
+
+        pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
+            if case let .failure(error) = completion {
+                debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
+                self.processError(APSError.pumpError(error))
+            } else {
                 debug(.apsManager, "Bolus succeeded")
                 if !isSMB {
                     self.determineBasal().sink { _ in }.store(in: &self.lifetime)
                 }
-            case let .failure(error):
-                debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             }
-        }
+
+            self.bolusReporter = pump.createBolusProgressReporter(reportingOn: self.processQueue)
+            self.bolusReporter?.addObserver(self)
+        } receiveValue: { _ in }
+            .store(in: &lifetime)
     }
 
     func enactTempBasal(rate: Double, duration: TimeInterval) {
@@ -253,6 +305,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
             case let .failure(error):
                 debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
+                self.processError(APSError.pumpError(error))
             }
         }
     }
@@ -366,18 +419,21 @@ final class BaseAPSManager: APSManager, Injectable {
         guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
             isLooping.send(false)
             debug(.apsManager, "Suggestion not found")
+            processError(APSError.apsError(message: "Suggestion not found"))
             return
         }
 
         guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
             isLooping.send(false)
             debug(.apsManager, "Suggestion expired")
+            processError(APSError.apsError(message: "Suggestion expired"))
             return
         }
 
         guard let pump = pumpManager, verifyStatus() else {
             isLooping.send(false)
-            debug(.apsManager, "Invalid pump status")
+            debug(.apsManager, "Invalid pump state")
+            processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
             return
         }
 
@@ -409,13 +465,15 @@ final class BaseAPSManager: APSManager, Injectable {
                 if case let .failure(error) = completion {
                     debug(.apsManager, "Loop failed with error: \(error.localizedDescription)")
                     self?.reportEnacted(suggestion: suggested, received: false)
+                    self?.processError(APSError.pumpError(error))
                 } else {
                     self?.reportEnacted(suggestion: suggested, received: true)
                 }
                 self?.isLooping.send(false)
             } receiveValue: {
                 debug(.apsManager, "Loop succeeded")
-                self.lastLoopDate.send(Date())
+                self.lastError.send(nil)
+                self.lastLoopDate = Date()
             }.store(in: &lifetime)
     }
 
@@ -434,6 +492,11 @@ final class BaseAPSManager: APSManager, Injectable {
             nightscout.uploadStatus()
         }
     }
+
+    private func processError(_ error: Error) {
+        debug(.apsManager, "\(error.localizedDescription)")
+        lastError.send(error)
+    }
 }
 
 private extension PumpManager {
@@ -502,6 +565,17 @@ extension BaseAPSManager: PumpManagerStatusObserver {
     }
 }
 
+extension BaseAPSManager: DoseProgressObserver {
+    func doseProgressReporterDidUpdate(_ doseProgressReporter: DoseProgressReporter) {
+        bolusProgress.send(Decimal(doseProgressReporter.progress.percentComplete))
+        if doseProgressReporter.progress.isComplete {
+            bolusReporter?.removeObserver(self)
+            bolusReporter = nil
+            bolusProgress.send(nil)
+        }
+    }
+}
+
 extension PumpManagerStatus {
     var pumpStatus: PumpStatus {
         let bolusing = bolusState != .noBolus

+ 13 - 3
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -13,9 +13,11 @@ protocol DeviceDataManager {
     var pumpManager: PumpManagerUI? { get set }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
     var recommendsLoop: PassthroughSubject<Void, Never> { get }
+    var errorSubject: PassthroughSubject<Error, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     func heartbeat(date: Date, force: Bool)
+    func createBolusProgressReporter() -> DoseProgressReporter?
 }
 
 private let staticPumpManagers: [PumpManagerUI.Type] = [
@@ -42,6 +44,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         .distantPast
 
     let recommendsLoop = PassthroughSubject<Void, Never>()
+    let errorSubject = PassthroughSubject<Error, Never>()
 
     var pumpManager: PumpManagerUI? {
         didSet {
@@ -84,6 +87,10 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 
     @SyncAccess(lock: accessLock) private var pumpUpdateInProgress = false
 
+    func createBolusProgressReporter() -> DoseProgressReporter? {
+        pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
+    }
+
     func heartbeat(date: Date, force: Bool) {
         if force {
             updatePumpData()
@@ -210,7 +217,8 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {}
 
     func pumpManager(_: PumpManager, didError error: PumpManagerError) {
-        debug(.deviceManager, "error: \(error.localizedDescription)")
+        debug(.deviceManager, "error: \(error.localizedDescription), reason: \(String(describing: error.failureReason))")
+        errorSubject.send(error)
         pumpUpdateInProgress = false
     }
 
@@ -297,9 +305,11 @@ extension BaseDeviceDataManager: DeviceManagerDelegate {
         _: DeviceManager,
         logEventForDeviceIdentifier _: String?,
         type _: DeviceLogEntryType,
-        message _: String,
+        message: String,
         completion _: ((Error?) -> Void)?
-    ) {}
+    ) {
+        debug(.deviceManager, message)
+    }
 }
 
 // MARK: - AlertPresenter

+ 8 - 1
FreeAPS/Sources/Containers/ServiceContainer.swift

@@ -7,6 +7,13 @@ enum ServiceContainer: DependeciesContainer {
     static func register(container: Container) {
         container.register(NotificationCenter.self) { _ in Foundation.NotificationCenter.default }
         container.register(Broadcaster.self) { _ in BaseBroadcaster() }
-        container.register(GroupedIssueReporter.self) { _ in CollectionIssueReporter() }
+        container.register(GroupedIssueReporter.self) { _ in
+            let reporter = CollectionIssueReporter()
+            reporter.add(reporters: [
+                SimpleLogReporter()
+            ])
+            reporter.setup()
+            return reporter
+        }
     }
 }

+ 88 - 0
FreeAPS/Sources/Logger/IssueReporter/SimpleLogReporter.swift

@@ -0,0 +1,88 @@
+import Foundation
+import SwiftDate
+
+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.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 ?? "" }
+}

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -65,7 +65,7 @@ extension Bolus {
                             label: { Text("Continue without bolus") }
                         } else {
                             Button { viewModel.addWithoutBolus() }
-                            label: { Text("Add insulin without actual bolusing") }
+                            label: { Text("Add insulin without actually bolusing") }
                         }
                     }
                 }

+ 20 - 10
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -34,6 +34,9 @@ extension Home {
         @Published var pumpExpiresAtDate: Date?
         @Published var tempTarget: TempTarget?
         @Published var setupPump = false
+        @Published var errorMessage: String? = nil
+        @Published var errorDate: Date? = nil
+        @Published var bolusProgress: Decimal?
 
         @Published var allowManualTemp = false
         private(set) var units: GlucoseUnits = .mmolL
@@ -54,16 +57,9 @@ extension Home {
             units = settingsManager.settings.units
             allowManualTemp = !settingsManager.settings.closedLoop
             closedLoop = settingsManager.settings.closedLoop
-            setStatusTitle()
-
-            if closedLoop,
-               enactedSuggestion?.deliverAt == suggestion?.deliverAt, suggestion?.rate != nil || suggestion?.units != nil
-            {
-                lastLoopDate = enactedSuggestion?.timestamp ?? .distantPast
-            } else {
-                lastLoopDate = suggestion?.timestamp ?? .distantPast
-            }
+            lastLoopDate = apsManager.lastLoopDate
 
+            setStatusTitle()
             setupCurrentTempTarget()
 
             broadcaster.register(GlucoseObserver.self, observer: self)
@@ -91,7 +87,7 @@ extension Home {
                 .assign(to: \.isLooping, on: self)
                 .store(in: &lifetime)
 
-            apsManager.lastLoopDate
+            apsManager.lastLoopDateSubject
                 .receive(on: DispatchQueue.main)
                 .assign(to: \.lastLoopDate, on: self)
                 .store(in: &lifetime)
@@ -105,6 +101,20 @@ extension Home {
                 .receive(on: DispatchQueue.main)
                 .assign(to: \.pumpExpiresAtDate, on: self)
                 .store(in: &lifetime)
+
+            apsManager.lastError
+                .receive(on: DispatchQueue.main)
+                .map { error in
+                    self.errorDate = error == nil ? nil : Date()
+                    return error?.localizedDescription
+                }
+                .assign(to: \.errorMessage, on: self)
+                .store(in: &lifetime)
+
+            apsManager.bolusProgress
+                .receive(on: DispatchQueue.main)
+                .assign(to: \.bolusProgress, on: self)
+                .store(in: &lifetime)
         }
 
         func addCarbs() {

+ 9 - 1
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -35,7 +35,7 @@ struct LoopView: View {
             if isLooping {
                 Text("looping").font(.caption2)
             } else if actualSuggestion?.timestamp != nil {
-                Text("\(Int((timerDate.timeIntervalSince(lastLoopDate) - Config.lag) / 60) + 1) min ago").font(.caption2)
+                Text(timeString).font(.caption2)
                     .foregroundColor(.secondary)
             } else {
                 Text("--").font(.caption2).foregroundColor(.secondary)
@@ -43,6 +43,14 @@ struct LoopView: View {
         }
     }
 
+    private var timeString: String {
+        let minAgo = Int((timerDate.timeIntervalSince(lastLoopDate) - Config.lag) / 60) + 1
+        if minAgo > 1440 {
+            return "--"
+        }
+        return "\(minAgo) min ago"
+    }
+
     private var color: Color {
         guard actualSuggestion?.timestamp != nil else {
             return .loopGray

+ 18 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -20,6 +20,12 @@ extension Home {
             return formatter
         }
 
+        private var dateFormatter: DateFormatter {
+            let dateFormatter = DateFormatter()
+            dateFormatter.timeStyle = .short
+            return dateFormatter
+        }
+
         var header: some View {
             HStack(alignment: .bottom) {
                 Spacer()
@@ -130,6 +136,11 @@ extension Home {
                     }
                 }
                 Spacer()
+                if let progress = viewModel.bolusProgress {
+                    Text("Bolus " + (numberFormatter.string(from: progress * 100 as NSNumber)!) + "%")
+                        .font(.system(size: 12, weight: .bold)).foregroundColor(.insulin)
+                        .padding(.trailing, 8)
+                }
             }
             .frame(maxWidth: .infinity, maxHeight: 30)
         }
@@ -247,6 +258,13 @@ extension Home {
                     Text(viewModel.statusTitle).foregroundColor(.white)
                         .padding(.bottom, 4)
                     Text(viewModel.suggestion?.reason ?? "No sugestion found").font(.caption).foregroundColor(.white)
+
+                    if let errorMessage = viewModel.errorMessage, let date = viewModel.errorDate {
+                        Text("Error at \(dateFormatter.string(from: date))").foregroundColor(.white)
+                            .padding(.bottom, 4)
+                            .padding(.top, 8)
+                        Text(errorMessage).font(.caption).foregroundColor(.white)
+                    }
                 }
                 .padding()
 

+ 15 - 0
FreeAPS/Sources/Modules/Settings/SettingsViewModel.swift

@@ -4,6 +4,7 @@ extension Settings {
     class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: SettingsProvider {
         @Injected() private var settingsManager: SettingsManager!
         @Injected() private var broadcaster: Broadcaster!
+        @Injected() private var fileManager: FileManager!
         @Published var closedLoop = false
 
         @Published var debugOptions = false
@@ -24,6 +25,20 @@ extension Settings {
 
             buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
         }
+
+        func logItems() -> [URL] {
+            var items: [URL] = []
+
+            if fileManager.fileExists(atPath: SimpleLogReporter.logFile) {
+                items.append(URL(fileURLWithPath: SimpleLogReporter.logFile))
+            }
+
+            if fileManager.fileExists(atPath: SimpleLogReporter.logFilePrev) {
+                items.append(URL(fileURLWithPath: SimpleLogReporter.logFilePrev))
+            }
+
+            return items
+        }
     }
 }
 

+ 11 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -3,6 +3,7 @@ import SwiftUI
 extension Settings {
     struct RootView: BaseView {
         @EnvironmentObject var viewModel: ViewModel<Provider>
+        @State private var showShareSheet = false
 
         var body: some View {
             Form {
@@ -82,6 +83,16 @@ extension Settings {
                         }
                     }
                 }
+
+                Section {
+                    Text("Share logs").chevronCell()
+                        .onTapGesture {
+                            showShareSheet = true
+                        }
+                }
+            }
+            .sheet(isPresented: $showShareSheet) {
+                ShareSheet(activityItems: viewModel.logItems())
             }
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: viewModel.hideModal))

+ 1 - 1
FreeAPS/Sources/Services/Network/NetworkService.swift

@@ -14,7 +14,7 @@ enum NetworkError: Error, LocalizedError {
 
 struct NetworkService {
     func run(_ request: URLRequest) -> AnyPublisher<Data, Error> {
-        debug(.nightscout, "\(request.httpMethod!) Request at \(request.url!.absoluteString)")
+        debug(.nightscout, "\(request.httpMethod!)  ***\(request.url!.path)\(request.url!.query.map { "?" + $0 } ?? "")")
         return URLSession.shared
             .dataTaskPublisher(for: request)
             .tryMap { data, response in