diff --git a/TempTrack.xcodeproj/project.pbxproj b/TempTrack.xcodeproj/project.pbxproj index 030a551..28485dc 100644 --- a/TempTrack.xcodeproj/project.pbxproj +++ b/TempTrack.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 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 */; }; + E2A553FF2A3A1024005204C3 /* DayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FE2A3A1024005204C3 /* DayView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -77,6 +78,7 @@ 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 = ""; }; + E2A553FE2A3A1024005204C3 /* DayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -178,6 +180,7 @@ 88404DDC2A2F587400D30244 /* HistoryListRow.swift */, 88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */, E2A553FA2A39C82D005204C3 /* LogView.swift */, + E2A553FE2A3A1024005204C3 /* DayView.swift */, ); path = Views; sourceTree = ""; @@ -303,6 +306,7 @@ 88404DD82A2F381B00D30244 /* HistoryList.swift in Sources */, 88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */, 88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */, + E2A553FF2A3A1024005204C3 /* DayView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate b/TempTrack.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate index 161a99e..c52ec07 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 e316a77..2860dcc 100644 --- a/TempTrack/Bluetooth/BluetoothClient.swift +++ b/TempTrack/Bluetooth/BluetoothClient.swift @@ -142,16 +142,16 @@ final class BluetoothClient: ObservableObject { guard let deviceInfo else { return } - guard !deviceInfo.hasDeviceStartTimeSet || deviceInfo.clockOffset > minimumOffsetToUpdateDeviceClock else { + guard !deviceInfo.hasDeviceStartTimeSet || abs(deviceInfo.clockOffset) > minimumOffsetToUpdateDeviceClock else { return } guard !openRequests.contains(where: { if case .setDeviceStartTime = $0 { return true }; return false }) else { return } - let time = deviceInfo.deviceStartTime.seconds + let time = deviceInfo.calculatedDeviceStartTime.seconds addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time)) - log.info("Setting device start time to \(time) s (\(Date().seconds) current)") + log.info("Setting device start time to \(time) s (correcting offset of \(Int(deviceInfo.clockOffset)) s)") } // MARK: Data transfer diff --git a/TempTrack/Bluetooth/DeviceInfo.swift b/TempTrack/Bluetooth/DeviceInfo.swift index 4676adc..5154233 100644 --- a/TempTrack/Bluetooth/DeviceInfo.swift +++ b/TempTrack/Bluetooth/DeviceInfo.swift @@ -52,9 +52,14 @@ struct DeviceInfo { var clockOffset: TimeInterval { // Measurements are performed on device start (-1) and also count next measurement (+1) - let nextMeasurementTime = deviceStartTime.adding(seconds: (totalNumberOfMeasurements) * measurementInterval) + let nextMeasurementTime = deviceStartTime.adding(seconds: totalNumberOfMeasurements * measurementInterval) return nextMeasurement.timeIntervalSince(nextMeasurementTime) } + + var calculatedDeviceStartTime: Date { + let runtime = totalNumberOfMeasurements * measurementInterval + return nextMeasurement.adding(seconds: -runtime) + } } extension DeviceInfo { diff --git a/TempTrack/ContentView.swift b/TempTrack/ContentView.swift index 2a2a200..96e81de 100644 --- a/TempTrack/ContentView.swift +++ b/TempTrack/ContentView.swift @@ -104,7 +104,7 @@ struct ContentView: View { self.showHistory = true } label: { TemperatureHistoryChart(points: $storage.recentMeasurements) - .frame(height: 150) + .frame(height: 300) .background(Color.white.opacity(0.1)) .cornerRadius(8) } diff --git a/TempTrack/Storage/TemperatureStorage.swift b/TempTrack/Storage/TemperatureStorage.swift index c0bf202..c423070 100644 --- a/TempTrack/Storage/TemperatureStorage.swift +++ b/TempTrack/Storage/TemperatureStorage.swift @@ -63,6 +63,7 @@ final class TemperatureStorage: ObservableObject { } ensureExistenceOfFolder() + recalculateDailyCounts() } private func ensureExistenceOfFolder() { @@ -105,15 +106,15 @@ final class TemperatureStorage: ObservableObject { } let yesterdayValues = loadMeasurements(for: dateIndexOfStart) .filter { $0.date >= startDate } - recentMeasurements = yesterdayValues + todayValues + recentMeasurements = todayValues + yesterdayValues 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)") + recentMeasurements = (measurements + recentMeasurements) + .filter { $0.id > startDate } + log.info("\(recentMeasurements.count) recent measurements (with \(measurements.count) new entries)") } private func loadMeasurements(for date: Date) -> [TemperatureMeasurement] { @@ -143,17 +144,19 @@ final class TemperatureStorage: ObservableObject { func add(_ measurements: [TemperatureMeasurement]) { let lastDate = self.newestMeasurementDate.seconds - let newerValues = measurements.filter { $0.id > lastDate } + let newerValues: [TemperatureMeasurement] = measurements.filter { $0.id > lastDate }.reversed() let newValues = newerValues.splitByDate() - log.info("Adding \(newValues.count) of \(measurements.count) measurements") + log.info("Adding \(newerValues.count) of \(measurements.count) measurements") for (dateIndex, values) in newValues { let count = saveNew(values, for: dateIndex) setDailyCount(count, for: dateIndex) - //log.info("Day \(dateIndex): \(count) values") } saveDailyCounts() updateLastMeasurements(measurements) + if let newest = newerValues.max()?.id { + newestMeasurementTime = newest + } } func removeMeasurements(for dateIndex: Int) { @@ -176,7 +179,7 @@ final class TemperatureStorage: ObservableObject { */ private func saveNew(_ measurements: [TemperatureMeasurement], for dateIndex: Int) -> Int { let fileName = fileName(for: dateIndex) - let values = loadMeasurements(from: fileName) + measurements + let values = measurements + loadMeasurements(from: fileName) save(values, for: fileName) return values.count } @@ -184,7 +187,7 @@ final class TemperatureStorage: ObservableObject { private func save(_ measurements: [TemperatureMeasurement], for fileName: String) { let fileUrl = fileUrl(for: fileName) do { - let data = try BinaryEncoder.encode(measurements.sorted()) + let data = try BinaryEncoder.encode(measurements.sorted().reversed()) try data.write(to: fileUrl) } catch { log.error("Failed to save \(fileName): \(error)") @@ -203,10 +206,7 @@ final class TemperatureStorage: ObservableObject { private func add(dailyCount count: Int, for dateIndex: Int) { let entry = MeasurementDailyCount(dateIndex: dateIndex, count: count) - guard let index = dailyMeasurementCounts.firstIndex(where: { $0.dateIndex < dateIndex }) else { - dailyMeasurementCounts.append(entry) - return - } + let index = dailyMeasurementCounts.firstIndex(where: { $0.dateIndex > dateIndex }) ?? 0 dailyMeasurementCounts.insert(entry, at: index) } @@ -240,8 +240,10 @@ final class TemperatureStorage: ObservableObject { self.dailyMeasurementCounts = measurements.reduce(into: [Int: Int]()) { counts, value in let index = value.date.dateIndex counts[index] = (counts[index] ?? 0) + 1 - }.map { MeasurementDailyCount(dateIndex: $0.key, count: $0.value) } - .sorted() + } + .map { MeasurementDailyCount(dateIndex: $0.key, count: $0.value) } + .sorted() + .reversed() } func recalculateDailyCounts() { @@ -260,13 +262,13 @@ final class TemperatureStorage: ObservableObject { self.dailyMeasurementCounts = newValues .map { .init(dateIndex: $0.key, count: $0.value) } .sorted() + .reversed() log.info("Daily counts recalculated from \(files.count) files") } } catch { log.error("Failed to load daily counts: \(error)") } } - } private extension Array where Element == TemperatureMeasurement { diff --git a/TempTrack/Temperature/TemperatureDataTransfer.swift b/TempTrack/Temperature/TemperatureDataTransfer.swift index eb3316d..f31d2fd 100644 --- a/TempTrack/Temperature/TemperatureDataTransfer.swift +++ b/TempTrack/Temperature/TemperatureDataTransfer.swift @@ -56,12 +56,16 @@ final class TemperatureDataTransfer { func add(data: Data, offset: Int, count: Int) { guard currentByteIndex == offset else { - log.warning("Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)") + log.warning("Transfer: Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)") return } + if data.count != count { + log.warning("Transfer: Expected \(count) bytes, received only \(data.count)") + } dataBuffer.append(data) currentByteIndex += data.count processBytes() + log.info("Transfer: \(currentByteIndex) bytes (added \(data.count)), \(measurements.count) points") } private func processBytes() { @@ -83,6 +87,10 @@ final class TemperatureDataTransfer { func completeTransfer() { processBytes() + if !dataBuffer.isEmpty { + log.warning("\(dataBuffer.count) bytes remaining in transfer buffer") + } + log.info("Transfer: \(currentByteIndex) bytes, \(measurements.count) points") } private func addRelative(byte: UInt8) { diff --git a/TempTrack/Temperature/TemperatureMeasurement.swift b/TempTrack/Temperature/TemperatureMeasurement.swift index 1bb8f02..80eba0d 100644 --- a/TempTrack/Temperature/TemperatureMeasurement.swift +++ b/TempTrack/Temperature/TemperatureMeasurement.swift @@ -27,7 +27,7 @@ struct TemperatureMeasurement: Identifiable { return sensor1.optionalValue } guard let s1 = sensor1.optionalValue else { - return nil + return s0 } return max(s0, s1) } @@ -37,10 +37,27 @@ struct TemperatureMeasurement: Identifiable { return sensor1.optionalValue } guard let s1 = sensor1.optionalValue else { - return nil + return s0 } return min(s0, s1) } + + var averageValue: Double? { + guard let s0 = sensor0.optionalValue else { + return sensor1.optionalValue + } + guard let s1 = sensor1.optionalValue else { + return s0 + } + return (s0 + s1) / 2 + } + + var displayText: String { + guard let averageValue else { + return "-" + } + return String(format: "%.1f °C", averageValue) + } } extension TemperatureMeasurement { diff --git a/TempTrack/Views/DayView.swift b/TempTrack/Views/DayView.swift new file mode 100644 index 0000000..e55e445 --- /dev/null +++ b/TempTrack/Views/DayView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +private let df: DateFormatter = { + let df = DateFormatter() + df.dateStyle = .short + df.timeStyle = .medium + return df +}() + +struct DayView: View { + + let dateIndex: Int + + @EnvironmentObject + var storage: TemperatureStorage + + var entries: [TemperatureMeasurement] { + storage.loadMeasurements(for: dateIndex) + } + + var body: some View { + TemperatureDayOverview(points: entries) + List(entries) { entry in + HStack { + Text(df.string(from: entry.date)) + Spacer() + Text(entry.displayText) + } + } + } +} + +struct DayView_Previews: PreviewProvider { + static var previews: some View { + DayView(dateIndex: Date.now.dateIndex) + .environmentObject(TemperatureStorage.mock) + } +} diff --git a/TempTrack/Views/HistoryList.swift b/TempTrack/Views/HistoryList.swift index f7352e5..7d96465 100644 --- a/TempTrack/Views/HistoryList.swift +++ b/TempTrack/Views/HistoryList.swift @@ -9,16 +9,17 @@ struct HistoryList: View { NavigationView { List(storage.dailyMeasurementCounts) { day in NavigationLink(destination: { - TemperatureDayOverview(storage: storage, dateIndex: day.dateIndex) + DayView(dateIndex: day.dateIndex) + .environmentObject(storage) }) { HistoryListRow(entry: day) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { deleteRow(for: day.dateIndex) } label: { - Label("Delete", systemSymbol: .pencil) + Label("Delete", systemSymbol: .trash) } - .tint(.purple) + .tint(.red) } } } diff --git a/TempTrack/Views/LogView.swift b/TempTrack/Views/LogView.swift index f0f59db..48e0201 100644 --- a/TempTrack/Views/LogView.swift +++ b/TempTrack/Views/LogView.swift @@ -1,17 +1,17 @@ import SwiftUI +private let df: DateFormatter = { + let df = DateFormatter() + df.dateStyle = .short + df.timeStyle = .medium + return df +}() + 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) { diff --git a/TempTrack/Views/TemperatureDayOverview.swift b/TempTrack/Views/TemperatureDayOverview.swift index 8b28ceb..1cb2b3b 100644 --- a/TempTrack/Views/TemperatureDayOverview.swift +++ b/TempTrack/Views/TemperatureDayOverview.swift @@ -2,14 +2,14 @@ import SwiftUI import Charts struct TemperatureDayOverview: View { - - let storage: TemperatureStorage - - @State - var points: [TemperatureMeasurement] = [] + + let points: [TemperatureMeasurement] + + init(points: [TemperatureMeasurement]) { + self.points = points + } init(storage: TemperatureStorage, dateIndex: Int) { - self.storage = storage let points = storage.loadMeasurements(for: dateIndex) self.points = points update() diff --git a/TempTrack/Views/TemperatureHistoryChart.swift b/TempTrack/Views/TemperatureHistoryChart.swift index d9e0108..0c28e87 100644 --- a/TempTrack/Views/TemperatureHistoryChart.swift +++ b/TempTrack/Views/TemperatureHistoryChart.swift @@ -34,7 +34,7 @@ struct TemperatureHistoryChart: View { .chartXAxis(.hidden) .chartLegend(.hidden) .chartYAxis { - AxisMarks(position: .trailing, values: .automatic) { value in + AxisMarks(position: .trailing, values: .stride(by: 10)) { value in AxisValueLabel(multiLabelAlignment: .trailing) { if let intValue = value.as(Int.self) { Text("\(intValue)°")