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 0e9a75d..161a99e 100644 Binary files a/TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate and b/TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate differ 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() }