From 2cb94a12be3c9e00edcd98777a2a72868dc3a1e3 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 14 Jun 2023 16:16:56 +0200 Subject: [PATCH] Add log view, manual transfer --- TempTrack.xcodeproj/project.pbxproj | 12 ++++ .../xcshareddata/swiftpm/Package.resolved | 4 +- .../UserInterfaceState.xcuserstate | Bin 44975 -> 60563 bytes TempTrack/Bluetooth/BluetoothClient.swift | 57 +++++++++++------- TempTrack/Bluetooth/DeviceManager.swift | 46 +++++++------- TempTrack/ContentView.swift | 33 +++++++--- TempTrack/Storage/Log.swift | 48 +++++++++++++++ TempTrack/Storage/LogEntry.swift | 33 ++++++++++ TempTrack/Storage/TemperatureStorage.swift | 40 +++++++----- .../Temperature/TemperatureDataTransfer.swift | 2 +- TempTrack/Views/LogView.swift | 35 +++++++++++ TempTrack/Views/TemperatureDayOverview.swift | 1 - 12 files changed, 237 insertions(+), 74 deletions(-) create mode 100644 TempTrack/Storage/Log.swift create mode 100644 TempTrack/Storage/LogEntry.swift create mode 100644 TempTrack/Views/LogView.swift diff --git a/TempTrack.xcodeproj/project.pbxproj b/TempTrack.xcodeproj/project.pbxproj index ca7b645..030a551 100644 --- a/TempTrack.xcodeproj/project.pbxproj +++ b/TempTrack.xcodeproj/project.pbxproj @@ -38,6 +38,9 @@ 88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; }; E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */; }; E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */; }; + E2A553F92A399F58005204C3 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553F82A399F58005204C3 /* Log.swift */; }; + E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FA2A39C82D005204C3 /* LogView.swift */; }; + E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FC2A39C86B005204C3 /* LogEntry.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -71,6 +74,9 @@ 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = ""; }; E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureHistoryChart.swift; sourceTree = ""; }; + E2A553F82A399F58005204C3 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; + E2A553FA2A39C82D005204C3 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; }; + E2A553FC2A39C86B005204C3 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -91,6 +97,8 @@ children = ( 88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */, 88CDE0672A2698B400114294 /* TemperatureStorage.swift */, + E2A553F82A399F58005204C3 /* Log.swift */, + E2A553FC2A39C86B005204C3 /* LogEntry.swift */, ); path = Storage; sourceTree = ""; @@ -169,6 +177,7 @@ 88404DD72A2F381B00D30244 /* HistoryList.swift */, 88404DDC2A2F587400D30244 /* HistoryListRow.swift */, 88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */, + E2A553FA2A39C82D005204C3 /* LogView.swift */, ); path = Views; sourceTree = ""; @@ -281,6 +290,8 @@ 88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */, 88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */, 88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */, + E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */, + E2A553F92A399F58005204C3 /* Log.swift in Sources */, E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */, 88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */, E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */, @@ -288,6 +299,7 @@ 88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */, 88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */, 88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */, + E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */, 88404DD82A2F381B00D30244 /* HistoryList.swift in Sources */, 88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */, 88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */, diff --git a/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3658a74..b4fdc68 100644 --- a/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TempTrack.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", "state" : { - "revision" : "2bcd249b49178247e6b52bac7d67d6e338a40cee", - "version" : "4.1.0" + "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", + "version" : "4.1.1" } } ], diff --git a/TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate b/TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate index 0e9a75dccde2c891461d062d1c493c2bc4ff6659..161a99ee6b33c750075d1242c61abd6f1357ac09 100644 GIT binary patch literal 60563 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 ITs7bSALrK*ivR!s literal 44975 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 BOaK4? diff --git a/TempTrack/Bluetooth/BluetoothClient.swift b/TempTrack/Bluetooth/BluetoothClient.swift index 88ef494..e316a77 100644 --- a/TempTrack/Bluetooth/BluetoothClient.swift +++ b/TempTrack/Bluetooth/BluetoothClient.swift @@ -10,6 +10,17 @@ final class BluetoothClient: ObservableObject { private let connection = DeviceManager() private let storage: TemperatureStorage + + var hasInfo: Bool { + deviceInfo != nil + } + + var isConnected: Bool { + if case .configured = deviceState { + return true + } + return false + } init(storage: TemperatureStorage, deviceInfo: DeviceInfo? = nil) { self.storage = storage @@ -24,7 +35,7 @@ final class BluetoothClient: ObservableObject { @Published private(set) var deviceState: DeviceState = .disconnected { didSet { - print("State: \(deviceState)") + log.info("State: \(deviceState)") if case .configured = deviceState { startRegularUpdates() } else { @@ -37,7 +48,7 @@ final class BluetoothClient: ObservableObject { private(set) var deviceInfo: DeviceInfo? { didSet { updateDeviceTimeIfNeeded() - collectRecordedData() + // collectRecordedData() if let deviceInfo, let runningTransfer { runningTransfer.update(info: deviceInfo) let next = runningTransfer.nextRequest() @@ -68,7 +79,7 @@ final class BluetoothClient: ObservableObject { guard dataUpdateTimer == nil else { return } - print("Starting updates") + log.info("Starting updates") dataUpdateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] timer in guard let self = self else { timer.invalidate() @@ -87,7 +98,7 @@ final class BluetoothClient: ObservableObject { dataUpdateTimer.invalidate() runningRequest = nil self.dataUpdateTimer = nil - print("Ending updates") + log.info("Ending updates") } // MARK: Requests @@ -102,7 +113,7 @@ final class BluetoothClient: ObservableObject { let next = openRequests.removeFirst() guard connection.send(next.serialized) else { - print("Failed to start request \(next)") + log.warning("Failed to start request \(next)") performNextRequest() return } @@ -115,11 +126,11 @@ final class BluetoothClient: ObservableObject { } let type = request.byte if let runningRequest, runningRequest.byte == type { - print("Skipping duplicate request \(request)") + log.info("Skipping duplicate request \(request)") return } guard !openRequests.contains(where: { $0.byte == type }) else { - print("Skipping duplicate request \(request)") + log.info("Skipping duplicate request \(request)") return } openRequests.append(request) @@ -140,7 +151,7 @@ final class BluetoothClient: ObservableObject { } let time = deviceInfo.deviceStartTime.seconds addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time)) - print("Setting device start time to \(time) s (\(Date().seconds) current)") + log.info("Setting device start time to \(time) s (\(Date().seconds) current)") } // MARK: Data transfer @@ -148,15 +159,15 @@ final class BluetoothClient: ObservableObject { @discardableResult func collectRecordedData() -> Bool { guard runningTransfer == nil else { - print("Transfer already running") + log.info("Transfer already running") return false } guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else { - print("Transfer already in scheduled") + log.info("Transfer already scheduled") return false } guard let info = deviceInfo else { - print("No device info to start transfer") + log.warning("No device info to start transfer") return false } guard info.numberOfStoredMeasurements > 0 else { @@ -166,14 +177,14 @@ final class BluetoothClient: ObservableObject { let transfer = TemperatureDataTransfer(info: info) runningTransfer = transfer let next = transfer.nextRequest() - print("Starting transfer") + log.info("Starting transfer") addRequest(next) return true } private func didReceive(data: Data, offset: Int, count: Int) { guard let runningTransfer else { - print("No running transfer to process device data") + log.warning("No running transfer to process device data") return // TODO: Start new transfer? } runningTransfer.add(data: data, offset: offset, count: count) @@ -183,7 +194,7 @@ final class BluetoothClient: ObservableObject { private func decode(info: Data) { guard let newInfo = try? DeviceInfo(info: info) else { - print("Failed to decode device info") + log.error("Failed to decode device info") return } self.deviceInfo = newInfo @@ -197,25 +208,25 @@ extension BluetoothClient: DeviceManagerDelegate { performNextRequest() } guard let runningRequest else { - print("No request active, but \(data) received") + log.warning("No request active, but \(data) received") return } self.runningRequest = nil guard data.count > 0 else { - print("No response data for request \(runningRequest)") + log.error("No response data for request \(runningRequest)") return } guard let type = BluetoothResponseType(rawValue: data[0]) else { - print("Unknown response \(data[0]) for request \(runningRequest)") + log.error("Unknown response \(data[0]) for request \(runningRequest)") return } switch type { case .success: break case .responseInProgress: - print("Device is busy for \(runningRequest)") + log.info("Device is busy for \(runningRequest)") // Retry the request addRequest(runningRequest) return @@ -226,7 +237,7 @@ extension BluetoothClient: DeviceManagerDelegate { addRequest(.getInfo) return } - print("Request \(runningRequest) received non-matching responde about number of bytes to delete") + log.error("Request \(runningRequest) received non-matching responde about number of bytes to delete") case .responseTooLarge: guard case .getRecordingData = runningRequest else { // If requesting bytes fails due to the response size, @@ -234,9 +245,9 @@ extension BluetoothClient: DeviceManagerDelegate { addRequest(.getInfo) return } - print("Unexpectedly exceeded payload size for request \(runningRequest)") + log.error("Unexpectedly exceeded payload size for request \(runningRequest)") default: - print("Unknown response \(data[0]) for request \(runningRequest)") + log.error("Unknown response \(data[0]) for request \(runningRequest)") // If clearing the recording buffer fails due to byte mismatch, // then requesting new info will resolve the mismatch, and the transfer will be resumed @@ -255,14 +266,14 @@ extension BluetoothClient: DeviceManagerDelegate { didClearDeviceStorage() case .setDeviceStartTime: - print("Device time set") + log.info("Device time set") break } } private func didClearDeviceStorage() { guard let runningTransfer else { - print("No running transfer after clearing device storage") + log.warning("No running transfer after clearing device storage") return } runningTransfer.completeTransfer() diff --git a/TempTrack/Bluetooth/DeviceManager.swift b/TempTrack/Bluetooth/DeviceManager.swift index 6c9f036..62d5566 100644 --- a/TempTrack/Bluetooth/DeviceManager.swift +++ b/TempTrack/Bluetooth/DeviceManager.swift @@ -31,7 +31,7 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate { func connect() -> Bool { switch state { case .bluetoothDisabled: - print("Can't connect, bluetooth disabled") + log.info("Can't connect, bluetooth disabled") return false case .disconnected, .bluetoothEnabled: break @@ -91,7 +91,6 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate { } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - //print("Found device '\(peripheral.name ?? "NO_NAME")'") peripheral.delegate = self manager.connect(peripheral) manager.stopScan() @@ -107,30 +106,30 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate { connect() case .unsupported: state = .bluetoothDisabled - print("Bluetooth is not supported") + log.info("Bluetooth is not supported") case .unknown: state = .bluetoothDisabled - print("Bluetooth state is unknown") + log.info("Bluetooth state is unknown") case .resetting: state = .bluetoothDisabled - print("Bluetooth is resetting") + log.info("Bluetooth is resetting") case .unauthorized: state = .bluetoothDisabled - print("Bluetooth is not authorized") + log.info("Bluetooth is not authorized") @unknown default: state = .bluetoothDisabled - print("Unknown state \(central.state)") + log.warning("Unknown state \(central.state)") } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - //print("Connected to " + peripheral.name!) + log.info("Connected to " + peripheral.name!) peripheral.discoverServices([DeviceManager.serviceUUID]) state = .discoveringServices(device: peripheral) } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - print("Disconnected from " + peripheral.name!) + log.info("Disconnected from " + peripheral.name!) state = .disconnected // Attempt to reconnect if shouldConnectIfPossible { @@ -139,9 +138,9 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate { } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - print("Failed to connect device '\(peripheral.name ?? "NO_NAME")'") + log.warning("Failed to connect device '\(peripheral.name ?? "NO_NAME")'") if let error = error { - print(error) + log.warning(error.localizedDescription) } state = manager.isScanning ? .scanning : .disconnected // Attempt to reconnect @@ -155,12 +154,12 @@ extension DeviceManager: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard let services = peripheral.services, !services.isEmpty else { - print("No services found for device '\(peripheral.name ?? "NO_NAME")'") + log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'") manager.cancelPeripheralConnection(peripheral) return } guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else { - print("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})") + log.error("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})") manager.cancelPeripheralConnection(peripheral) return } @@ -170,18 +169,18 @@ extension DeviceManager: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { - print("Failed to discover characteristics: \(error)") + log.error("Failed to discover characteristics: \(error)") manager.cancelPeripheralConnection(peripheral) return } guard let characteristics = service.characteristics, !characteristics.isEmpty else { - print("No characteristics found for device") + log.error("No characteristics found for device") manager.cancelPeripheralConnection(peripheral) return } for characteristic in characteristics { guard characteristic.uuid == DeviceManager.characteristicUUID else { - print("Unused characteristic \(characteristic.uuid.uuidString)") + log.warning("Unused characteristic \(characteristic.uuid.uuidString)") continue } state = .configured(device: peripheral, characteristic: characteristic) @@ -191,35 +190,34 @@ extension DeviceManager: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - print("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)") + log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)") } - //print("Peripheral did write value for \(characteristic.uuid.uuidString)") } func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { if let error = error { - print("Failed to get RSSI: \(error)") + log.warning("Failed to get RSSI: \(error)") return } lastRSSI = RSSI.intValue - print("RSSI: \(lastRSSI)") + log.info("RSSI: \(lastRSSI)") } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - print("Failed to read value update: \(error)") + log.error("Failed to read value update: \(error)") return } guard case .configured(device: _, characteristic: let storedCharacteristic) = state else { - print("Received data while not properly configured") + log.warning("Received data while not properly configured") return } guard characteristic.uuid == storedCharacteristic.uuid else { - print("Read unknown characteristic \(characteristic.uuid.uuidString)") + log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)") return } guard let data = characteristic.value else { - print("No data") + log.warning("No data") return } delegate?.deviceManager(didReceive: data) diff --git a/TempTrack/ContentView.swift b/TempTrack/ContentView.swift index 4babb6c..2a2a200 100644 --- a/TempTrack/ContentView.swift +++ b/TempTrack/ContentView.swift @@ -23,13 +23,12 @@ struct ContentView: View { @State var showHistory = false + @State + var showLog = false + init() { } - - var hasDeviceInfo: Bool { - bluetoothClient.deviceInfo != nil - } var averageTemperature: Double? { let t1 = bluetoothClient.deviceInfo?.sensor1?.optionalValue @@ -110,17 +109,33 @@ struct ContentView: View { .cornerRadius(8) } HStack(alignment: .center) { + Button { + self.showLog.toggle() + } label: { + Image(systemSymbol: .textBubble) + .font(.system(size: 30, weight: .light)) + .foregroundColor(.white) + } + Spacer() Button { self.showDeviceInfo = true } label: { - if hasDeviceInfo { + if bluetoothClient.hasInfo { Image(systemSymbol: .iphone) - .font(.system(size: 30, weight: .regular)) + .font(.system(size: 30, weight: .light)) } Text(bluetoothClient.deviceState.text) } - .disabled(!hasDeviceInfo) + .disabled(!bluetoothClient.hasInfo) .foregroundColor(.white) + Spacer() + Button { + bluetoothClient.collectRecordedData() + } label: { + Image(systemSymbol: .arrowUpArrowDownCircle) + .font(.system(size: 30, weight: .light)) + .foregroundColor(.white) + }.disabled(!bluetoothClient.isConnected) }.padding() } @@ -136,6 +151,10 @@ struct ContentView: View { HistoryList() .environmentObject(storage) } + .sheet(isPresented: $showLog) { + LogView() + .environmentObject(log) + } .background(backgroundGradient) } } diff --git a/TempTrack/Storage/Log.swift b/TempTrack/Storage/Log.swift new file mode 100644 index 0000000..f7c06c5 --- /dev/null +++ b/TempTrack/Storage/Log.swift @@ -0,0 +1,48 @@ +import Foundation + +let log = Log() + +final class Log: ObservableObject { + + private let df: DateFormatter + + init() { + df = .init() + df.dateStyle = .short + df.timeStyle = .medium + } + + enum Level: String { + case info = "INFO" + case warning = "WARN" + case error = "ERROR" + } + + @Published + var logEntries: [LogEntry] = [] + + func info(_ message: String) { + log(.info, message) + } + + func warning(_ message: String) { + log(.warning, message) + } + + func error(_ message: String) { + log(.error, message) + } + + func log(_ level: Level, _ message: String) { + let entry = LogEntry(level: level, message: message) + logEntries.insert(entry, at: 0) + print(entry) + } +} + +extension Log.Level: CustomStringConvertible { + + var description: String { + rawValue + } +} diff --git a/TempTrack/Storage/LogEntry.swift b/TempTrack/Storage/LogEntry.swift new file mode 100644 index 0000000..0a70036 --- /dev/null +++ b/TempTrack/Storage/LogEntry.swift @@ -0,0 +1,33 @@ +import Foundation + +struct LogEntry: Identifiable { + + let id: TimeInterval + + let date: Date + + let level: Log.Level + + let message: String + + init(date: Date = Date(), level: Log.Level, message: String) { + self.id = date.timeIntervalSince1970 + self.date = date + self.level = level + self.message = message + } +} + +private let df: DateFormatter = { + let df = DateFormatter() + df.dateStyle = .short + df.timeStyle = .medium + return df +}() + +extension LogEntry: CustomStringConvertible { + + var description: String { + "[\(df.string(from: date))][\(level.rawValue)] \(message)" + } +} diff --git a/TempTrack/Storage/TemperatureStorage.swift b/TempTrack/Storage/TemperatureStorage.swift index 928a5f7..c0bf202 100644 --- a/TempTrack/Storage/TemperatureStorage.swift +++ b/TempTrack/Storage/TemperatureStorage.swift @@ -57,6 +57,7 @@ final class TemperatureStorage: ObservableObject { if lastMeasurements.isEmpty { loadLastMeasurements() + loadDailyCounts() } else { setDailyCounts(from: lastMeasurements) } @@ -71,7 +72,7 @@ final class TemperatureStorage: ObservableObject { do { try fm.createDirectory(at: storageFolder, withIntermediateDirectories: true) } catch { - print("Failed to create folder: \(error)") + log.error("Failed to create folder: \(error)") } } @@ -99,17 +100,20 @@ final class TemperatureStorage: ObservableObject { let dateIndexOfStart = startDate.dateIndex guard todayIndex != dateIndexOfStart else { recentMeasurements = todayValues + log.info("Loaded \(recentMeasurements.count) recent measurements") return } let yesterdayValues = loadMeasurements(for: dateIndexOfStart) .filter { $0.date >= startDate } recentMeasurements = yesterdayValues + todayValues + log.info("Loaded \(recentMeasurements.count) recent measurements") } private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) { let startDate = Date().addingTimeInterval(-lastValueInterval).seconds let new = recentMeasurements + measurements recentMeasurements = Array(new.drop { $0.id < startDate }) + log.info("\(recentMeasurements.count) recent measurements (of \(measurements.count) new entries)") } private func loadMeasurements(for date: Date) -> [TemperatureMeasurement] { @@ -123,30 +127,30 @@ final class TemperatureStorage: ObservableObject { private func loadMeasurements(from fileName: String) -> [TemperatureMeasurement] { let fileUrl = fileUrl(for: fileName) guard fm.fileExists(atPath: fileUrl.path) else { - print("No measurements for \(fileName)") + log.info("No measurements for \(fileName)") return [] } do { let content = try Data(contentsOf: fileUrl) let points: [TemperatureMeasurement] = try BinaryDecoder.decode(from: content) - print("Loaded \(points.count) points for \(fileName)") + log.info("Loaded \(points.count) points from \(fileName)") return points } catch { - print("Failed to read file \(fileName): \(error)") + log.error("Failed to read file \(fileName): \(error)") return [] } } func add(_ measurements: [TemperatureMeasurement]) { let lastDate = self.newestMeasurementDate.seconds - let newValues = measurements - .filter { $0.id > lastDate } - .splitByDate() + let newerValues = measurements.filter { $0.id > lastDate } + let newValues = newerValues.splitByDate() + log.info("Adding \(newValues.count) of \(measurements.count) measurements") for (dateIndex, values) in newValues { let count = saveNew(values, for: dateIndex) setDailyCount(count, for: dateIndex) - print("Day \(dateIndex): \(count) values") + //log.info("Day \(dateIndex): \(count) values") } saveDailyCounts() updateLastMeasurements(measurements) @@ -155,7 +159,7 @@ final class TemperatureStorage: ObservableObject { func removeMeasurements(for dateIndex: Int) { let fileUrl = fileUrl(for: dateIndex) guard fm.fileExists(atPath: fileUrl.path) else { - print("No measurements for \(fileUrl.lastPathComponent)") + log.warning("No measurements for \(fileUrl.lastPathComponent)") return } do { @@ -163,7 +167,7 @@ final class TemperatureStorage: ObservableObject { dailyMeasurementCounts = dailyMeasurementCounts.filter { $0.dateIndex != dateIndex } recentMeasurements = recentMeasurements.filter { $0.date.dateIndex != dateIndex } } catch { - print("Failed to delete \(fileUrl.lastPathComponent): \(error)") + log.error("Failed to delete \(fileUrl.lastPathComponent): \(error)") } } @@ -183,7 +187,7 @@ final class TemperatureStorage: ObservableObject { let data = try BinaryEncoder.encode(measurements.sorted()) try data.write(to: fileUrl) } catch { - print("Failed to save \(fileName): \(error)") + log.error("Failed to save \(fileName): \(error)") } } @@ -219,7 +223,7 @@ final class TemperatureStorage: ObservableObject { let data = try Data(contentsOf: overviewFileUrl) dailyMeasurementCounts = try BinaryDecoder.decode(from: data) } catch { - print("Failed to load overview: \(error)") + log.error("Failed to load overview: \(error)") } } @@ -228,7 +232,7 @@ final class TemperatureStorage: ObservableObject { let data = try BinaryEncoder.encode(dailyMeasurementCounts) try data.write(to: overviewFileUrl) } catch { - print("Failed to write overview: \(error)") + log.error("Failed to write overview: \(error)") } } @@ -242,9 +246,12 @@ final class TemperatureStorage: ObservableObject { func recalculateDailyCounts() { do { - let newValues: [Int: Int] = try fm.contentsOfDirectory(atPath: storageFolder.path) + let files = try fm.contentsOfDirectory(atPath: storageFolder.path) + let newValues: [Int: Int] = files .reduce(into: [:]) { counts, fileName in - guard let dateIndex = Int(fileName) else { + let dateString = fileName.replacingOccurrences(of: ".bin", with: "") + guard let dateIndex = Int(dateString) else { + log.warning("Found file with invalid name \(fileName)") return } counts[dateIndex] = loadMeasurements(from: fileName).count @@ -253,9 +260,10 @@ final class TemperatureStorage: ObservableObject { self.dailyMeasurementCounts = newValues .map { .init(dateIndex: $0.key, count: $0.value) } .sorted() + log.info("Daily counts recalculated from \(files.count) files") } } catch { - print("Failed to load daily counts: \(error)") + log.error("Failed to load daily counts: \(error)") } } diff --git a/TempTrack/Temperature/TemperatureDataTransfer.swift b/TempTrack/Temperature/TemperatureDataTransfer.swift index 1be3161..eb3316d 100644 --- a/TempTrack/Temperature/TemperatureDataTransfer.swift +++ b/TempTrack/Temperature/TemperatureDataTransfer.swift @@ -56,7 +56,7 @@ final class TemperatureDataTransfer { func add(data: Data, offset: Int, count: Int) { guard currentByteIndex == offset else { - print("Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)") + log.warning("Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)") return } dataBuffer.append(data) diff --git a/TempTrack/Views/LogView.swift b/TempTrack/Views/LogView.swift new file mode 100644 index 0000000..f0f59db --- /dev/null +++ b/TempTrack/Views/LogView.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct LogView: View { + + @EnvironmentObject + var log: Log + + private let df: DateFormatter = { + let df = DateFormatter() + df.dateStyle = .short + df.timeStyle = .medium + return df + }() + + var body: some View { + List(log.logEntries) { entry in + VStack(alignment: .leading) { + HStack { + Text(entry.level.description) + Spacer() + Text(df.string(from: entry.date)) + }.font(.footnote) + Text(entry.message) + } + } + + } +} + +struct LogView_Previews: PreviewProvider { + static var previews: some View { + LogView() + .environmentObject(Log()) + } +} diff --git a/TempTrack/Views/TemperatureDayOverview.swift b/TempTrack/Views/TemperatureDayOverview.swift index d212a8b..8b28ceb 100644 --- a/TempTrack/Views/TemperatureDayOverview.swift +++ b/TempTrack/Views/TemperatureDayOverview.swift @@ -11,7 +11,6 @@ struct TemperatureDayOverview: View { init(storage: TemperatureStorage, dateIndex: Int) { self.storage = storage let points = storage.loadMeasurements(for: dateIndex) - print("Loaded \(points.count) points for date \(dateIndex)") self.points = points update() }