Ivan Valkou 5 năm trước cách đây
mục cha
commit
6eade6462a

+ 52 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -176,6 +176,13 @@
 		38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D0B3B525EBE24900CB6E88 /* Battery.swift */; };
 		38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */; };
 		38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E989DC25F5021400C0CED0 /* PumpStatus.swift */; };
+		38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1B25F52C9300C0CED0 /* Signpost.swift */; };
+		38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1C25F52C9300C0CED0 /* Logger.swift */; };
+		38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1E25F52C9300C0CED0 /* IssueReporter.swift */; };
+		38E98A2725F52C9300C0CED0 /* CollectionIssueReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A2025F52C9300C0CED0 /* CollectionIssueReporter.swift */; };
+		38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A2225F52C9300C0CED0 /* Error+Extensions.swift */; };
+		38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A2C25F52DC400C0CED0 /* NSLocking+Extensions.swift */; };
+		38E98A3025F52FF700C0CED0 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A2F25F52FF700C0CED0 /* Config.swift */; };
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */; };
@@ -750,6 +757,13 @@
 		38D0B3B525EBE24900CB6E88 /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = "<group>"; };
 		38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsEntry.swift; sourceTree = "<group>"; };
 		38E989DC25F5021400C0CED0 /* PumpStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatus.swift; sourceTree = "<group>"; };
+		38E98A1B25F52C9300C0CED0 /* Signpost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Signpost.swift; sourceTree = "<group>"; };
+		38E98A1C25F52C9300C0CED0 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
+		38E98A1E25F52C9300C0CED0 /* IssueReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueReporter.swift; sourceTree = "<group>"; };
+		38E98A2025F52C9300C0CED0 /* CollectionIssueReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionIssueReporter.swift; sourceTree = "<group>"; };
+		38E98A2225F52C9300C0CED0 /* Error+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Error+Extensions.swift"; sourceTree = "<group>"; };
+		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>"; };
 		38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetsStorage.swift; sourceTree = "<group>"; };
 		38FCF3D525E8FDF40078B0D1 /* MD5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MD5.swift; sourceTree = "<group>"; };
 		38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FreeAPSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -924,8 +938,10 @@
 			children = (
 				3811DEDE25C9E2DD00A708ED /* Application */,
 				3811DF0A25CAAAA500A708ED /* APS */,
+				38E98A3225F5300800C0CED0 /* Config */,
 				3811DEBD25C9D99900A708ED /* Containers */,
 				388E5A5A25B6F05F0019842D /* Helpers */,
+				38E98A1A25F52C9300C0CED0 /* Logger */,
 				388E5A5925B6F0250019842D /* Models */,
 				3811DE0325C9D31700A708ED /* Modules */,
 				3811DE1425C9D40400A708ED /* Router */,
@@ -1301,6 +1317,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				38E98A2C25F52DC400C0CED0 /* NSLocking+Extensions.swift */,
 				3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */,
 				38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */,
 				3811DE5425C9D4D500A708ED /* Formatters.swift */,
@@ -1440,6 +1457,34 @@
 			path = SwiftNotificationCenter;
 			sourceTree = "<group>";
 		};
+		38E98A1A25F52C9300C0CED0 /* Logger */ = {
+			isa = PBXGroup;
+			children = (
+				38E98A1B25F52C9300C0CED0 /* Signpost.swift */,
+				38E98A1C25F52C9300C0CED0 /* Logger.swift */,
+				38E98A1D25F52C9300C0CED0 /* IssueReporter */,
+				38E98A2225F52C9300C0CED0 /* Error+Extensions.swift */,
+			);
+			path = Logger;
+			sourceTree = "<group>";
+		};
+		38E98A1D25F52C9300C0CED0 /* IssueReporter */ = {
+			isa = PBXGroup;
+			children = (
+				38E98A1E25F52C9300C0CED0 /* IssueReporter.swift */,
+				38E98A2025F52C9300C0CED0 /* CollectionIssueReporter.swift */,
+			);
+			path = IssueReporter;
+			sourceTree = "<group>";
+		};
+		38E98A3225F5300800C0CED0 /* Config */ = {
+			isa = PBXGroup;
+			children = (
+				38E98A2F25F52FF700C0CED0 /* Config.swift */,
+			);
+			path = Config;
+			sourceTree = "<group>";
+		};
 		38FCF3EE25E9028E0078B0D1 /* FreeAPSTests */ = {
 			isa = PBXGroup;
 			children = (
@@ -2132,6 +2177,7 @@
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
+				38E98A2725F52C9300C0CED0 /* CollectionIssueReporter.swift in Sources */,
 				3811DEE825CA063400A708ED /* Injected.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
@@ -2156,6 +2202,7 @@
 				3811DE7F25C9D6D300A708ED /* LoginBuilder.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
+				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				3811DE3325C9D49500A708ED /* HomeBuilder.swift in Sources */,
@@ -2183,6 +2230,7 @@
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				3811DEB725C9D88300A708ED /* AuthorizationManager.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
+				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				3811DF0825CAAA4700A708ED /* ServiceContainer.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DE4D25C9D4B800A708ED /* AuthotizedRootViewModel.swift in Sources */,
@@ -2198,6 +2246,7 @@
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
 				CDB87FA71A93F3739D3D338E /* NightscoutConfigBuilder.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
+				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
 				BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */,
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigViewModel.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
@@ -2217,6 +2266,7 @@
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorViewModel.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
+				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
 				CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */,
 				F215CAB49BA4B5A01C3BC6B6 /* ISFEditorBuilder.swift in Sources */,
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
@@ -2229,6 +2279,7 @@
 				69B9A368029F7EB39F525422 /* CREditorViewModel.swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
 				7F7017AA5C69838FB7E6FECE /* TargetsEditorBuilder.swift in Sources */,
+				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorViewModel.swift in Sources */,
@@ -2251,6 +2302,7 @@
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
+				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusViewModel.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
 				1FF95E8F785B28961EFDE5A9 /* ManualTempBasalBuilder.swift in Sources */,

+ 38 - 28
FreeAPS/Sources/APS/APSManager.swift

@@ -28,10 +28,7 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     private var openAPS: OpenAPS!
 
-    private var loopCancellable: AnyCancellable?
-    private var pumpCancellable: AnyCancellable?
-    private var enactCancellable: AnyCancellable?
-    private var remoteCancellable: AnyCancellable?
+    private var lifetime = Set<AnyCancellable>()
 
     var pumpManager: PumpManagerUI? {
         get { deviceDataManager.pumpManager }
@@ -54,10 +51,11 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func subscribe() {
-        pumpCancellable = deviceDataManager.recommendsLoop
+        deviceDataManager.recommendsLoop
             .sink { [weak self] in
                 self?.fetchAndLoop()
             }
+            .store(in: &lifetime)
         pumpManager?.addStatusObserver(self, queue: processQueue)
     }
 
@@ -67,18 +65,18 @@ final class BaseAPSManager: APSManager, Injectable {
             return
         }
 
-        remoteCancellable = nightscout.fetchAnnouncements()
+        nightscout.fetchAnnouncements()
             .sink { [weak self] in
                 if let recent = self?.announcementsStorage.recent(), recent.action != nil {
                     self?.enactAnnouncement(recent)
                 } else {
                     self?.loop()
                 }
-            }
+            }.store(in: &lifetime)
     }
 
     private func loop() {
-        loopCancellable = Publishers.CombineLatest3(
+        Publishers.CombineLatest3(
             nightscout.fetchGlucose(),
             nightscout.fetchCarbs(),
             nightscout.fetchTempTargets()
@@ -97,7 +95,7 @@ final class BaseAPSManager: APSManager, Injectable {
             if ok, self.settings.closedLoop {
                 self.enactSuggested()
             }
-        }
+        }.store(in: &lifetime)
     }
 
     private func verifyStatus() -> Bool {
@@ -113,7 +111,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private func determineBasal() -> AnyPublisher<Bool, Never> {
         guard let glucose = try? storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
-            print("Not enough glucose data")
+            debug(.apsManager, "Not enough glucose data")
             return Just(false).eraseToAnyPublisher()
         }
 
@@ -148,9 +146,9 @@ final class BaseAPSManager: APSManager, Injectable {
         pump.enactBolus(units: roundedAmout, automatic: false) { result in
             switch result {
             case .success:
-                print("Bolus succeeded")
+                debug(.apsManager, "Bolus succeeded")
             case let .failure(error):
-                print("Bolus failed with error: \(error.localizedDescription)")
+                debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             }
         }
     }
@@ -162,11 +160,11 @@ final class BaseAPSManager: APSManager, Injectable {
         pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration) { result in
             switch result {
             case .success:
-                print("Temp Basal succeeded")
+                debug(.apsManager, "Temp Basal succeeded")
                 let temp = TempBasal(duration: Int(duration / 60), rate: Decimal(rate), temp: .absolute, updatedAt: Date())
                 try? self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
             case let .failure(error):
-                print("Temp Basal failed with error: \(error.localizedDescription)")
+                debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
             }
         }
     }
@@ -181,7 +179,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private func enactAnnouncement(_ announcement: Announcement) {
         guard let action = announcement.action else {
-            print("Invalid Announcement action")
+            debug(.apsManager, "Invalid Announcement action")
             return
         }
         switch action {
@@ -192,10 +190,10 @@ final class BaseAPSManager: APSManager, Injectable {
             pumpManager?.enactBolus(units: Double(amount), automatic: false) { result in
                 switch result {
                 case .success:
-                    print("Announcement Bolus succeeded")
+                    debug(.apsManager, "Announcement Bolus succeeded")
                     self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
                 case let .failure(error):
-                    print("Announcement Bolus failed with error: \(error.localizedDescription)")
+                    debug(.apsManager, "Announcement Bolus failed with error: \(error.localizedDescription)")
                 }
             }
         case let .pump(pumpAction):
@@ -206,25 +204,25 @@ final class BaseAPSManager: APSManager, Injectable {
                 }
                 pumpManager?.suspendDelivery { error in
                     if let error = error {
-                        print("Pump not suspended by Announcement: \(error.localizedDescription)")
+                        debug(.apsManager, "Pump not suspended by Announcement: \(error.localizedDescription)")
                     } else {
-                        print("Pump suspended by Announcement")
+                        debug(.apsManager, "Pump suspended by Announcement")
                         self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
                     }
                 }
             case .resume:
                 pumpManager?.resumeDelivery { error in
                     if let error = error {
-                        print("Pump not resumed by Announcement: \(error.localizedDescription)")
+                        debug(.apsManager, "Pump not resumed by Announcement: \(error.localizedDescription)")
                     } else {
-                        print("Pump resumed by Announcement")
+                        debug(.apsManager, "Pump resumed by Announcement")
                         self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
                     }
                 }
             }
         case let .looping(closedLoop):
             settings.closedLoop = closedLoop
-            print("Closed loop \(closedLoop) by Announcement")
+            debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
             announcementsStorage.storeAnnouncements([announcement], enacted: true)
         case let .tempbasal(rate, duration):
             guard verifyStatus() else {
@@ -233,10 +231,10 @@ final class BaseAPSManager: APSManager, Injectable {
             pumpManager?.enactTempBasal(unitsPerHour: Double(rate), for: TimeInterval(duration) * 60) { result in
                 switch result {
                 case .success:
-                    print("Announcement TempBasal succeeded")
+                    debug(.apsManager, "Announcement TempBasal succeeded")
                     self.announcementsStorage.storeAnnouncements([announcement], enacted: true)
                 case let .failure(error):
-                    print("Announcement TempBasal failed with error: \(error.localizedDescription)")
+                    debug(.apsManager, "Announcement TempBasal failed with error: \(error.localizedDescription)")
                 }
             }
         }
@@ -301,18 +299,18 @@ final class BaseAPSManager: APSManager, Injectable {
                 .eraseToAnyPublisher()
         }()
 
-        enactCancellable = basalPublisher
+        basalPublisher
             .flatMap { bolusPublisher }
             .sink { completion in
                 if case let .failure(error) = completion {
-                    print("Loop failed with error: \(error.localizedDescription)")
+                    debug(.apsManager, "Loop failed with error: \(error.localizedDescription)")
                 }
             } receiveValue: { [weak self] in
-                print("Loop succeeded")
+                debug(.apsManager, "Loop succeeded")
                 if let rawSuggested = self?.storage.retrieveRaw(OpenAPS.Enact.suggested) {
                     try? self?.storage.save(rawSuggested, as: OpenAPS.Enact.enacted)
                 }
-            }
+            }.store(in: &lifetime)
     }
 }
 
@@ -343,6 +341,18 @@ private extension PumpManager {
         }.eraseToAnyPublisher()
     }
 
+    func suspendDelivery() -> AnyPublisher<Void, Error> {
+        Future { promise in
+            self.suspendDelivery { error in
+                if let error = error {
+                    promise(.failure(error))
+                } else {
+                    promise(.success(()))
+                }
+            }
+        }.eraseToAnyPublisher()
+    }
+
     func resumeDelivery() -> AnyPublisher<Void, Error> {
         Future { promise in
             self.resumeDelivery { error in

+ 7 - 7
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -87,9 +87,9 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     }
 
     func pumpManagerBLEHeartbeatDidFire(_ pumpManager: PumpManager) {
-        print("[DeviceDataManager] Pump Heartbeat")
+        debug(.deviceManager, "Pump Heartbeat")
         pumpManager.ensureCurrentPumpData {
-            print("[DeviceDataManager] Pump Data updated")
+            debug(.deviceManager, "Pump Data updated")
         }
     }
 
@@ -98,8 +98,8 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     }
 
     func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
-        print("[DeviceDataManager] New pump status Bolus: \(status.bolusState)")
-        print("[DeviceDataManager] New pump status Basal: \(String(describing: status.basalDeliveryState))")
+        debug(.deviceManager, "New pump status Bolus: \(status.bolusState)")
+        debug(.deviceManager, "New pump status Basal: \(String(describing: status.basalDeliveryState))")
     }
 
     func pumpManagerWillDeactivate(_: PumpManager) {
@@ -109,7 +109,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {}
 
     func pumpManager(_: PumpManager, didError error: PumpManagerError) {
-        print("[DeviceDataManager] error: \(error.localizedDescription)")
+        info(.deviceManager, "error: \(error.localizedDescription)")
     }
 
     func pumpManager(
@@ -132,7 +132,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             Error
         >) -> Void
     ) {
-        print("[DeviceDataManager] Reservoir Value \(units), at: \(date)")
+        debug(.deviceManager, "Reservoir Value \(units), at: \(date)")
         try? storage.save(Decimal(units), as: OpenAPS.Monitor.reservoir)
         let batteryPercent = Int((pumpManager?.status.pumpBatteryChargeRemaining ?? 1) * 100)
         let battery = Battery(percent: batteryPercent, string: batteryPercent >= 10 ? .normal : .low)
@@ -145,7 +145,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     }
 
     func pumpManagerRecommendsLoop(_: PumpManager) {
-        print("[DeviceDataManager] Recomends loop")
+        debug(.deviceManager, "Recomends loop")
         recommendsLoop.send()
     }
 

+ 4 - 4
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -64,7 +64,7 @@ final class OpenAPS {
                     microBolusAllowed: true,
                     reservoir: reservoir
                 )
-                print("SUGGESTED: \(suggested)")
+                debug(.openAPS, "SUGGESTED: \(suggested)")
 
                 try? self.storage.save(suggested, as: Enact.suggested)
 
@@ -91,7 +91,7 @@ final class OpenAPS {
                     temptargets: RawJSON.null
                 )
 
-                print("AUTOSENS: \(autosensResult)")
+                debug(.openAPS, "AUTOSENS: \(autosensResult)")
                 try? self.storage.save(autosensResult, as: Settings.autosense)
                 promise(.success(()))
             }
@@ -113,7 +113,7 @@ final class OpenAPS {
                     categorizeUamAsBasal: categorizeUamAsBasal,
                     tuneInsulinCurve: tuneInsulinCurve
                 )
-                print("AUTOTUNE PREP: \(autotunePreppedGlucose)")
+                debug(.openAPS, "AUTOTUNE PREP: \(autotunePreppedGlucose)")
 
                 let previousAutotune = try? self.storage.retrieve(Settings.autotune, as: RawJSON.self)
 
@@ -125,7 +125,7 @@ final class OpenAPS {
 
                 try? self.storage.save(autotuneResult, as: Settings.autotune)
 
-                print("AUTOTUNE RESULT: \(autotuneResult)")
+                debug(.openAPS, "AUTOTUNE RESULT: \(autotuneResult)")
                 promise(.success(()))
             }
         }

+ 6 - 0
FreeAPS/Sources/Config/Config.swift

@@ -0,0 +1,6 @@
+import Foundation
+
+enum Config {
+    static let treatWarningsAsErrors = true
+    static let withSignPosts = false
+}

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

@@ -7,5 +7,6 @@ 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() }
     }
 }

+ 23 - 0
FreeAPS/Sources/Helpers/NSLocking+Extensions.swift

@@ -0,0 +1,23 @@
+import Foundation
+
+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
+    }
+}

+ 5 - 0
FreeAPS/Sources/Logger/Error+Extensions.swift

@@ -0,0 +1,5 @@
+import Foundation
+
+extension Error {
+    var shouldReportNonFatalIssue: Bool { true }
+}

+ 54 - 0
FreeAPS/Sources/Logger/IssueReporter/CollectionIssueReporter.swift

@@ -0,0 +1,54 @@
+import Foundation
+import Swinject
+
+protocol GroupedIssueReporter: IssueReporter {
+    func add(reporters: [IssueReporter])
+    func remove(reporter: IssueReporter)
+}
+
+final class CollectionIssueReporter: GroupedIssueReporter {
+    private let reportersLock = NSRecursiveLock(label: "CollectionIssueReporter.reportersLock")
+    private var reporters: [IssueReporter] = []
+
+    func setup() {
+        reportersLock.perform {
+            reporters.forEach { $0.setup() }
+        }
+    }
+
+    func setUserIdentifier(_ identifier: String?) {
+        reportersLock.perform {
+            reporters.forEach { $0.setUserIdentifier(identifier) }
+        }
+    }
+
+    func reportNonFatalIssue(withName name: String, attributes: [String: String]) {
+        reportersLock.perform {
+            reporters.forEach { $0.reportNonFatalIssue(withName: name, attributes: attributes) }
+        }
+    }
+
+    func reportNonFatalIssue(withError error: NSError) {
+        reportersLock.perform {
+            reporters.forEach { $0.reportNonFatalIssue(withError: error) }
+        }
+    }
+
+    func log(_ category: String, _ message: String, file: String, function: String, line: UInt) {
+        reportersLock.perform {
+            reporters.forEach { $0.log(category, message, file: file, function: function, line: line) }
+        }
+    }
+
+    func add(reporters: [IssueReporter]) {
+        reportersLock.perform {
+            self.reporters.append(contentsOf: reporters)
+        }
+    }
+
+    func remove(reporter: IssueReporter) {
+        reportersLock.perform {
+            reporters.removeAll { $0 === reporter }
+        }
+    }
+}

+ 14 - 0
FreeAPS/Sources/Logger/IssueReporter/IssueReporter.swift

@@ -0,0 +1,14 @@
+import Foundation
+
+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)
+}

+ 305 - 0
FreeAPS/Sources/Logger/Logger.swift

@@ -0,0 +1,305 @@
+import os.log
+import os.signpost
+import UIKit
+
+var LoggerTestMode = false
+
+private let baseReporter = FreeAPSApp.resolver.resolve(GroupedIssueReporter.self)!
+private let router = FreeAPSApp.resolver.resolve(Router.self)!
+
+let loggerLock = NSRecursiveLock()
+
+func debug(
+    _ category: Logger.Category,
+    _ message: @autoclosure () -> String,
+    printToConsole: Bool = true,
+    file: String = #file,
+    function: String = #function,
+    line: UInt = #line
+) {
+    let msg = message()
+    DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
+        loggerLock.perform {
+            category.logger.debug(msg, printToConsole: printToConsole, file: file, function: function, line: line)
+        }
+    }.perform()
+}
+
+func info(
+    _ category: Logger.Category,
+    _ message: String,
+    file: String = #file,
+    function: String = #function,
+    line: UInt = #line
+) {
+    DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
+        loggerLock.perform {
+            category.logger.info(message, file: file, function: function, line: line)
+        }
+    }.perform()
+}
+
+func warning(
+    _ category: Logger.Category,
+    _ message: String,
+    description: String? = nil,
+    error maybeError: Swift.Error? = nil,
+    file: String = #file,
+    function: String = #function,
+    line: UInt = #line
+) {
+    DispatchWorkItem(qos: .userInteractive, flags: .enforceQoS) {
+        loggerLock.perform {
+            category.logger.warning(
+                message,
+                description: description,
+                error: maybeError,
+                file: file,
+                function: function,
+                line: line
+            )
+        }
+    }.perform()
+}
+
+func error(
+    _ category: Logger.Category,
+    _ message: String,
+    description: String? = nil,
+    error maybeError: Swift.Error? = nil,
+    file: String = #file,
+    function: String = #function,
+    line: UInt = #line
+) -> Never {
+    loggerLock.perform {
+        category.logger.errorWithoutFatalError(
+            message,
+            description: description,
+            error: maybeError,
+            file: file,
+            function: function,
+            line: line
+        )
+
+        fatalError(
+            "\(message) @ \(String(describing: description)) @ \(String(describing: maybeError)) @ \(file) @ \(function) @ \(line)"
+        )
+    }
+}
+
+func check(
+    _ condition: @autoclosure () -> Bool,
+    _ message: @autoclosure () -> String,
+    description: @autoclosure () -> String? = nil,
+    file: String = #file,
+    function: String = #function,
+    line: UInt = #line
+) {
+    guard !condition() else { return }
+    let msg = message()
+    let descr = description()
+    loggerLock.perform {
+        warning(.default, msg, description: descr, file: file.file, function: function, line: line)
+    }
+}
+
+final class Logger {
+    static let `default` = Logger(category: .default, reporter: baseReporter)
+    static let service = Logger(category: .service, reporter: baseReporter)
+    static let businessLogic = Logger(category: .businessLogic, reporter: baseReporter)
+    static let openAPS = Logger(category: .openAPS, reporter: baseReporter)
+    static let deviceManager = Logger(category: .openAPS, reporter: baseReporter)
+    static let apsManager = Logger(category: .openAPS, reporter: baseReporter)
+
+    enum Category: String {
+        case `default`
+        case service
+        case businessLogic
+        case openAPS
+        case deviceManager
+        case apsManager
+
+        var name: String {
+            rawValue.capitalized
+        }
+
+        var logger: Logger {
+            switch self {
+            case .default: return .default
+            case .service: return .service
+            case .businessLogic: return .businessLogic
+            case .openAPS: return .openAPS
+            case .deviceManager: return .deviceManager
+            case .apsManager: return .apsManager
+            }
+        }
+
+        fileprivate var log: OSLog {
+            let subsystem = Bundle.main.bundleIdentifier!
+            switch self {
+            case .default: return OSLog.default
+            case .apsManager,
+                 .businessLogic,
+                 .deviceManager,
+                 .openAPS,
+                 .service:
+                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 message = "INFO: \(message)"
+        os_log("%@ - %@ - %d %{public}@", log: log, type: .info, file.file, function, line, message)
+        reporter.log(category.name, message, file: file, function: function, line: line)
+
+        showAlert(message)
+    }
+
+    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)
+        if !LoggerTestMode, maybeError?.shouldReportNonFatalIssue ?? true {
+            reporter.reportNonFatalIssue(withError: loggerError.asNSError())
+            showAlert(message)
+        }
+    }
+
+    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)"
+        )
+    }
+
+    private func showAlert(_ message: String) {
+//        let category = self.category
+        DispatchQueue.main.async {
+            router.alertMessage.send(message)
+        }
+    }
+
+    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)
+        }
+    }
+}
+
+private extension String {
+    var file: String { components(separatedBy: "/").last ?? "" }
+}

+ 150 - 0
FreeAPS/Sources/Logger/Signpost.swift

@@ -0,0 +1,150 @@
+import os.log
+import os.signpost
+
+protocol RangeSignpost {
+    func end(_ format: @autoclosure () -> StaticString, _ arguments: @autoclosure () -> [String])
+    func end(_ format: @autoclosure () -> StaticString)
+}
+
+enum Signpost {
+    static func point(
+        _ category: Logger.Category,
+        name: StaticString,
+        format: StaticString? = nil,
+        arguments: @autoclosure () -> [String] = []
+    ) {
+        guard Config.withSignPosts else { return }
+        if #available(iOS 12.0, *) {
+            let log = category.logger.log
+            let signpostID = OSSignpostID(log: log)
+            set(.event, log: log, name: name, signpostID: signpostID, format, arguments())
+        }
+    }
+
+    static func perform<T>(
+        _: Logger.Category,
+        name: StaticString,
+        block: () throws -> T
+    ) rethrows -> T {
+        let signpost = Signpost.range(.businessLogic, name: name)
+        defer { signpost.end(name) }
+        return try block()
+    }
+
+    static func range(
+        _ category: Logger.Category,
+        name: StaticString
+    ) -> RangeSignpost {
+        guard Config.withSignPosts else { return EmptySignpost() }
+        if #available(iOS 12.0, *) {
+            return BaseRangeSignpost(category, name)
+        }
+        return EmptySignpost()
+    }
+
+    static func range(
+        _ category: Logger.Category,
+        name: StaticString,
+        format: StaticString,
+        arguments: @autoclosure () -> [String] = []
+    ) -> RangeSignpost {
+        guard Config.withSignPosts else { return EmptySignpost() }
+        if #available(iOS 12.0, *) {
+            return BaseRangeSignpost(category, name, format, arguments())
+        }
+        return EmptySignpost()
+    }
+
+    @available(iOS 12.0, *) fileprivate static func set(
+        _ type: OSSignpostType,
+        log: OSLog,
+        name: StaticString,
+        signpostID: OSSignpostID,
+        _ format: StaticString? = nil,
+        _ arguments: [String] = []
+    ) {
+        if let format = format {
+            switch arguments.count {
+            case 0: os_signpost(type, log: log, name: name, signpostID: signpostID, format)
+            case 1: os_signpost(type, log: log, name: name, signpostID: signpostID, format, arguments[0])
+            case 2: os_signpost(type, log: log, name: name, signpostID: signpostID, format, arguments[0], arguments[1])
+            case 3: os_signpost(
+                    type,
+                    log: log,
+                    name: name,
+                    signpostID: signpostID,
+                    format,
+                    arguments[0],
+                    arguments[1],
+                    arguments[2]
+                )
+            case 4: os_signpost(
+                    type,
+                    log: log,
+                    name: name,
+                    signpostID: signpostID,
+                    format,
+                    arguments[0],
+                    arguments[1],
+                    arguments[2],
+                    arguments[3]
+                )
+            case 5: os_signpost(
+                    type,
+                    log: log,
+                    name: name,
+                    signpostID: signpostID,
+                    format,
+                    arguments[0],
+                    arguments[1],
+                    arguments[2],
+                    arguments[3],
+                    arguments[4]
+                )
+            default: error(.service, "Signpost.set is not implemented for size.", description: "\(arguments.count)")
+            }
+        } else {
+            os_signpost(type, log: log, name: name, signpostID: signpostID)
+        }
+    }
+}
+
+struct EmptySignpost: RangeSignpost {
+    func end(_: @autoclosure () -> StaticString, _: @autoclosure () -> [String]) {}
+    func end(_: @autoclosure () -> StaticString) {}
+}
+
+@available(iOS 12.0, *) final class BaseRangeSignpost: RangeSignpost {
+    private let log: OSLog
+    private let signpostID: OSSignpostID
+    private let name: StaticString
+    private var endFormat: StaticString?
+    private var endArguments: [String] = []
+
+    init(_ category: Logger.Category, _ name: StaticString) {
+        log = category.logger.log
+        signpostID = OSSignpostID(log: log)
+        self.name = name
+        Signpost.set(.begin, log: log, name: name, signpostID: signpostID)
+    }
+
+    init(_ category: Logger.Category, _ name: StaticString, _ format: StaticString, _ arguments: [String]) {
+        log = category.logger.log
+        signpostID = OSSignpostID(log: log)
+        self.name = name
+        Signpost.set(.begin, log: log, name: name, signpostID: signpostID, format, arguments)
+    }
+
+    deinit {
+        Signpost.set(.end, log: log, name: name, signpostID: signpostID, endFormat, endArguments)
+    }
+
+    func end(_ format: @autoclosure () -> StaticString, _ arguments: @autoclosure () -> [String]) {
+        endFormat = format()
+        endArguments = arguments()
+    }
+
+    func end(_ format: @autoclosure () -> StaticString) {
+        end(format(), [])
+    }
+}

+ 10 - 1
FreeAPS/Sources/Modules/Main/MainViewModel.swift

@@ -7,8 +7,9 @@ extension Main {
         @Published private(set) var isAuthotized = false
         private(set) var modal: Modal?
         @Published var isModalPresented = false
+        @Published var isAlertPresented = false
+        @Published var alertMessage = ""
         @Published private(set) var scene: Scene!
-        private var nextModal: Modal?
 
         required init(provider: Provider, resolver: Resolver) {
             super.init(provider: provider, resolver: resolver)
@@ -44,6 +45,14 @@ extension Main {
                     self.router.mainModalScreen.send(nil)
                 }
                 .store(in: &lifetime)
+
+            router.alertMessage
+                .receive(on: DispatchQueue.main)
+                .sink { message in
+                    self.isAlertPresented = true
+                    self.alertMessage = message
+                }
+                .store(in: &lifetime)
         }
     }
 }

+ 7 - 0
FreeAPS/Sources/Modules/Main/View/MainRootView.swift

@@ -10,6 +10,13 @@ extension Main {
                     NavigationView { self.viewModel.modal!.view }
                         .navigationViewStyle(StackNavigationViewStyle())
                 }
+                .alert(isPresented: $viewModel.isAlertPresented) {
+                    Alert(
+                        title: Text("Important message"),
+                        message: Text(viewModel.alertMessage),
+                        dismissButton: .default(Text("Dismiss"))
+                    )
+                }
         }
     }
 }

+ 2 - 0
FreeAPS/Sources/Router/Router.swift

@@ -4,11 +4,13 @@ import Swinject
 
 protocol Router {
     var mainModalScreen: CurrentValueSubject<Screen?, Never> { get }
+    var alertMessage: PassthroughSubject<String, Never> { get }
     func view(for screen: Screen) -> AnyView
 }
 
 final class BaseRouter: Router {
     let mainModalScreen = CurrentValueSubject<Screen?, Never>(nil)
+    let alertMessage = PassthroughSubject<String, Never>()
 
     private let resolver: Resolver