Show all points for day

This commit is contained in:
Christoph Hagen 2023-06-14 17:52:43 +02:00
parent 2cb94a12be
commit 7cd697fb01
13 changed files with 116 additions and 41 deletions

View File

@ -41,6 +41,7 @@
E2A553F92A399F58005204C3 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553F82A399F58005204C3 /* Log.swift */; }; E2A553F92A399F58005204C3 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553F82A399F58005204C3 /* Log.swift */; };
E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FA2A39C82D005204C3 /* LogView.swift */; }; E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FA2A39C82D005204C3 /* LogView.swift */; };
E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FC2A39C86B005204C3 /* LogEntry.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 */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -77,6 +78,7 @@
E2A553F82A399F58005204C3 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; }; E2A553F82A399F58005204C3 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
E2A553FA2A39C82D005204C3 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; }; E2A553FA2A39C82D005204C3 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
E2A553FC2A39C86B005204C3 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; }; E2A553FC2A39C86B005204C3 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
E2A553FE2A3A1024005204C3 /* DayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -178,6 +180,7 @@
88404DDC2A2F587400D30244 /* HistoryListRow.swift */, 88404DDC2A2F587400D30244 /* HistoryListRow.swift */,
88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */, 88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */,
E2A553FA2A39C82D005204C3 /* LogView.swift */, E2A553FA2A39C82D005204C3 /* LogView.swift */,
E2A553FE2A3A1024005204C3 /* DayView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -303,6 +306,7 @@
88404DD82A2F381B00D30244 /* HistoryList.swift in Sources */, 88404DD82A2F381B00D30244 /* HistoryList.swift in Sources */,
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */, 88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */,
88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */, 88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */,
E2A553FF2A3A1024005204C3 /* DayView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -142,16 +142,16 @@ final class BluetoothClient: ObservableObject {
guard let deviceInfo else { guard let deviceInfo else {
return return
} }
guard !deviceInfo.hasDeviceStartTimeSet || deviceInfo.clockOffset > minimumOffsetToUpdateDeviceClock else { guard !deviceInfo.hasDeviceStartTimeSet || abs(deviceInfo.clockOffset) > minimumOffsetToUpdateDeviceClock else {
return return
} }
guard !openRequests.contains(where: { if case .setDeviceStartTime = $0 { return true }; return false }) else { guard !openRequests.contains(where: { if case .setDeviceStartTime = $0 { return true }; return false }) else {
return return
} }
let time = deviceInfo.deviceStartTime.seconds let time = deviceInfo.calculatedDeviceStartTime.seconds
addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time)) 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 // MARK: Data transfer

View File

@ -52,9 +52,14 @@ struct DeviceInfo {
var clockOffset: TimeInterval { var clockOffset: TimeInterval {
// Measurements are performed on device start (-1) and also count next measurement (+1) // 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) return nextMeasurement.timeIntervalSince(nextMeasurementTime)
} }
var calculatedDeviceStartTime: Date {
let runtime = totalNumberOfMeasurements * measurementInterval
return nextMeasurement.adding(seconds: -runtime)
}
} }
extension DeviceInfo { extension DeviceInfo {

View File

@ -104,7 +104,7 @@ struct ContentView: View {
self.showHistory = true self.showHistory = true
} label: { } label: {
TemperatureHistoryChart(points: $storage.recentMeasurements) TemperatureHistoryChart(points: $storage.recentMeasurements)
.frame(height: 150) .frame(height: 300)
.background(Color.white.opacity(0.1)) .background(Color.white.opacity(0.1))
.cornerRadius(8) .cornerRadius(8)
} }

View File

@ -63,6 +63,7 @@ final class TemperatureStorage: ObservableObject {
} }
ensureExistenceOfFolder() ensureExistenceOfFolder()
recalculateDailyCounts()
} }
private func ensureExistenceOfFolder() { private func ensureExistenceOfFolder() {
@ -105,15 +106,15 @@ final class TemperatureStorage: ObservableObject {
} }
let yesterdayValues = loadMeasurements(for: dateIndexOfStart) let yesterdayValues = loadMeasurements(for: dateIndexOfStart)
.filter { $0.date >= startDate } .filter { $0.date >= startDate }
recentMeasurements = yesterdayValues + todayValues recentMeasurements = todayValues + yesterdayValues
log.info("Loaded \(recentMeasurements.count) recent measurements") log.info("Loaded \(recentMeasurements.count) recent measurements")
} }
private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) { private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) {
let startDate = Date().addingTimeInterval(-lastValueInterval).seconds let startDate = Date().addingTimeInterval(-lastValueInterval).seconds
let new = recentMeasurements + measurements recentMeasurements = (measurements + recentMeasurements)
recentMeasurements = Array(new.drop { $0.id < startDate }) .filter { $0.id > startDate }
log.info("\(recentMeasurements.count) recent measurements (of \(measurements.count) new entries)") log.info("\(recentMeasurements.count) recent measurements (with \(measurements.count) new entries)")
} }
private func loadMeasurements(for date: Date) -> [TemperatureMeasurement] { private func loadMeasurements(for date: Date) -> [TemperatureMeasurement] {
@ -143,17 +144,19 @@ final class TemperatureStorage: ObservableObject {
func add(_ measurements: [TemperatureMeasurement]) { func add(_ measurements: [TemperatureMeasurement]) {
let lastDate = self.newestMeasurementDate.seconds 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() 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 { for (dateIndex, values) in newValues {
let count = saveNew(values, for: dateIndex) let count = saveNew(values, for: dateIndex)
setDailyCount(count, for: dateIndex) setDailyCount(count, for: dateIndex)
//log.info("Day \(dateIndex): \(count) values")
} }
saveDailyCounts() saveDailyCounts()
updateLastMeasurements(measurements) updateLastMeasurements(measurements)
if let newest = newerValues.max()?.id {
newestMeasurementTime = newest
}
} }
func removeMeasurements(for dateIndex: Int) { func removeMeasurements(for dateIndex: Int) {
@ -176,7 +179,7 @@ final class TemperatureStorage: ObservableObject {
*/ */
private func saveNew(_ measurements: [TemperatureMeasurement], for dateIndex: Int) -> Int { private func saveNew(_ measurements: [TemperatureMeasurement], for dateIndex: Int) -> Int {
let fileName = fileName(for: dateIndex) let fileName = fileName(for: dateIndex)
let values = loadMeasurements(from: fileName) + measurements let values = measurements + loadMeasurements(from: fileName)
save(values, for: fileName) save(values, for: fileName)
return values.count return values.count
} }
@ -184,7 +187,7 @@ final class TemperatureStorage: ObservableObject {
private func save(_ measurements: [TemperatureMeasurement], for fileName: String) { private func save(_ measurements: [TemperatureMeasurement], for fileName: String) {
let fileUrl = fileUrl(for: fileName) let fileUrl = fileUrl(for: fileName)
do { do {
let data = try BinaryEncoder.encode(measurements.sorted()) let data = try BinaryEncoder.encode(measurements.sorted().reversed())
try data.write(to: fileUrl) try data.write(to: fileUrl)
} catch { } catch {
log.error("Failed to save \(fileName): \(error)") log.error("Failed to save \(fileName): \(error)")
@ -203,10 +206,7 @@ final class TemperatureStorage: ObservableObject {
private func add(dailyCount count: Int, for dateIndex: Int) { private func add(dailyCount count: Int, for dateIndex: Int) {
let entry = MeasurementDailyCount(dateIndex: dateIndex, count: count) let entry = MeasurementDailyCount(dateIndex: dateIndex, count: count)
guard let index = dailyMeasurementCounts.firstIndex(where: { $0.dateIndex < dateIndex }) else { let index = dailyMeasurementCounts.firstIndex(where: { $0.dateIndex > dateIndex }) ?? 0
dailyMeasurementCounts.append(entry)
return
}
dailyMeasurementCounts.insert(entry, at: index) dailyMeasurementCounts.insert(entry, at: index)
} }
@ -240,8 +240,10 @@ final class TemperatureStorage: ObservableObject {
self.dailyMeasurementCounts = measurements.reduce(into: [Int: Int]()) { counts, value in self.dailyMeasurementCounts = measurements.reduce(into: [Int: Int]()) { counts, value in
let index = value.date.dateIndex let index = value.date.dateIndex
counts[index] = (counts[index] ?? 0) + 1 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() { func recalculateDailyCounts() {
@ -260,13 +262,13 @@ final class TemperatureStorage: ObservableObject {
self.dailyMeasurementCounts = newValues self.dailyMeasurementCounts = newValues
.map { .init(dateIndex: $0.key, count: $0.value) } .map { .init(dateIndex: $0.key, count: $0.value) }
.sorted() .sorted()
.reversed()
log.info("Daily counts recalculated from \(files.count) files") log.info("Daily counts recalculated from \(files.count) files")
} }
} catch { } catch {
log.error("Failed to load daily counts: \(error)") log.error("Failed to load daily counts: \(error)")
} }
} }
} }
private extension Array where Element == TemperatureMeasurement { private extension Array where Element == TemperatureMeasurement {

View File

@ -56,12 +56,16 @@ final class TemperatureDataTransfer {
func add(data: Data, offset: Int, count: Int) { func add(data: Data, offset: Int, count: Int) {
guard currentByteIndex == offset else { 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 return
} }
if data.count != count {
log.warning("Transfer: Expected \(count) bytes, received only \(data.count)")
}
dataBuffer.append(data) dataBuffer.append(data)
currentByteIndex += data.count currentByteIndex += data.count
processBytes() processBytes()
log.info("Transfer: \(currentByteIndex) bytes (added \(data.count)), \(measurements.count) points")
} }
private func processBytes() { private func processBytes() {
@ -83,6 +87,10 @@ final class TemperatureDataTransfer {
func completeTransfer() { func completeTransfer() {
processBytes() 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) { private func addRelative(byte: UInt8) {

View File

@ -27,7 +27,7 @@ struct TemperatureMeasurement: Identifiable {
return sensor1.optionalValue return sensor1.optionalValue
} }
guard let s1 = sensor1.optionalValue else { guard let s1 = sensor1.optionalValue else {
return nil return s0
} }
return max(s0, s1) return max(s0, s1)
} }
@ -37,10 +37,27 @@ struct TemperatureMeasurement: Identifiable {
return sensor1.optionalValue return sensor1.optionalValue
} }
guard let s1 = sensor1.optionalValue else { guard let s1 = sensor1.optionalValue else {
return nil return s0
} }
return min(s0, s1) 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 { extension TemperatureMeasurement {

View File

@ -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)
}
}

View File

@ -9,16 +9,17 @@ struct HistoryList: View {
NavigationView { NavigationView {
List(storage.dailyMeasurementCounts) { day in List(storage.dailyMeasurementCounts) { day in
NavigationLink(destination: { NavigationLink(destination: {
TemperatureDayOverview(storage: storage, dateIndex: day.dateIndex) DayView(dateIndex: day.dateIndex)
.environmentObject(storage)
}) { }) {
HistoryListRow(entry: day) HistoryListRow(entry: day)
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button { Button {
deleteRow(for: day.dateIndex) deleteRow(for: day.dateIndex)
} label: { } label: {
Label("Delete", systemSymbol: .pencil) Label("Delete", systemSymbol: .trash)
} }
.tint(.purple) .tint(.red)
} }
} }
} }

View File

@ -1,17 +1,17 @@
import SwiftUI import SwiftUI
private let df: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .medium
return df
}()
struct LogView: View { struct LogView: View {
@EnvironmentObject @EnvironmentObject
var log: Log var log: Log
private let df: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .medium
return df
}()
var body: some View { var body: some View {
List(log.logEntries) { entry in List(log.logEntries) { entry in
VStack(alignment: .leading) { VStack(alignment: .leading) {

View File

@ -3,13 +3,13 @@ import Charts
struct TemperatureDayOverview: View { struct TemperatureDayOverview: View {
let storage: TemperatureStorage let points: [TemperatureMeasurement]
@State init(points: [TemperatureMeasurement]) {
var points: [TemperatureMeasurement] = [] self.points = points
}
init(storage: TemperatureStorage, dateIndex: Int) { init(storage: TemperatureStorage, dateIndex: Int) {
self.storage = storage
let points = storage.loadMeasurements(for: dateIndex) let points = storage.loadMeasurements(for: dateIndex)
self.points = points self.points = points
update() update()

View File

@ -34,7 +34,7 @@ struct TemperatureHistoryChart: View {
.chartXAxis(.hidden) .chartXAxis(.hidden)
.chartLegend(.hidden) .chartLegend(.hidden)
.chartYAxis { .chartYAxis {
AxisMarks(position: .trailing, values: .automatic) { value in AxisMarks(position: .trailing, values: .stride(by: 10)) { value in
AxisValueLabel(multiLabelAlignment: .trailing) { AxisValueLabel(multiLabelAlignment: .trailing) {
if let intValue = value.as(Int.self) { if let intValue = value.as(Int.self) {
Text("\(intValue)°") Text("\(intValue)°")