- Restore curve without bucketIndex-based discontinuities - Reduce restore noise and avoid redundant restore loops
@@ -8,11 +8,13 @@ |
||
| 8 | 8 |
|
| 9 | 9 |
import CloudKit |
| 10 | 10 |
import CoreData |
| 11 |
+import OSLog |
|
| 11 | 12 |
import UIKit |
| 12 | 13 |
import UserNotifications |
| 13 | 14 |
|
| 14 | 15 |
//let btSerial = BluetoothSerial(delegate: BSD()) |
| 15 | 16 |
let appData = AppData() |
| 17 |
+private let restoreLogger = Logger(subsystem: "ro.xdev.USB-Meter", category: "Restore") |
|
| 16 | 18 |
enum Constants {
|
| 17 | 19 |
static let chartUnderscan: CGFloat = 0.5 |
| 18 | 20 |
static let chartOverscan: CGFloat = 1 - chartUnderscan |
@@ -32,6 +34,10 @@ public func track(_ message: String = "", file: String = #file, function: String |
||
| 32 | 34 |
print("\(hour):\(minutes):\(seconds) - \(file):\(line) - \(function) \(message)")
|
| 33 | 35 |
} |
| 34 | 36 |
|
| 37 |
+public func restoreTrace(_ message: String) {
|
|
| 38 |
+ restoreLogger.debug("\(message, privacy: .public)")
|
|
| 39 |
+} |
|
| 40 |
+ |
|
| 35 | 41 |
private func shouldEmitTrackMessage(_ message: String, file: String, function: String) -> Bool {
|
| 36 | 42 |
#if DEBUG |
| 37 | 43 |
if ProcessInfo.processInfo.environment["USB_METER_VERBOSE_LOGS"] == "1" {
|
@@ -284,6 +284,10 @@ final class AppData : ObservableObject {
|
||
| 284 | 284 |
} |
| 285 | 285 |
|
| 286 | 286 |
if let cachedSummary = cachedActiveChargeSessionSummary(for: normalizedMAC) {
|
| 287 |
+ if let persistedSummary = chargeInsightsStore?.activeChargeSessionSummary(forMeterMACAddress: normalizedMAC), |
|
| 288 |
+ persistedSummary.aggregatedSamples.count > cachedSummary.aggregatedSamples.count {
|
|
| 289 |
+ return persistedSummary |
|
| 290 |
+ } |
|
| 287 | 291 |
return cachedSummary |
| 288 | 292 |
} |
| 289 | 293 |
return chargeInsightsStore?.activeChargeSessionSummary(forMeterMACAddress: normalizedMAC) |
@@ -497,7 +501,7 @@ final class AppData : ObservableObject {
|
||
| 497 | 501 |
guard let activeSession = activeChargeSessionSummary(for: meter.btSerial.macAddress.description) else {
|
| 498 | 502 |
return |
| 499 | 503 |
} |
| 500 |
- guard activeSession.status == .active else {
|
|
| 504 |
+ guard activeSession.status.isOpen else {
|
|
| 501 | 505 |
return |
| 502 | 506 |
} |
| 503 | 507 |
meter.restoreChargeMonitoringIfNeeded(from: activeSession) |
@@ -10,6 +10,8 @@ import Foundation |
||
| 10 | 10 |
import CoreGraphics |
| 11 | 11 |
|
| 12 | 12 |
class Measurements : ObservableObject {
|
| 13 |
+ private static let restoredSampleDiscontinuityThreshold: TimeInterval = 90 |
|
| 14 |
+ |
|
| 13 | 15 |
struct EnergyProjectionSnapshot {
|
| 14 | 16 |
let accumulatedEnergy: Double |
| 15 | 17 |
let observedDuration: TimeInterval |
@@ -311,10 +313,11 @@ class Measurements : ObservableObject {
|
||
| 311 | 313 |
accumulatedEnergyValue = energy.samplePoints.last?.value ?? 0 |
| 312 | 314 |
} |
| 313 | 315 |
|
| 316 |
+ @discardableResult |
|
| 314 | 317 |
func restorePersistedChargeSessionSamplesIfNeeded( |
| 315 | 318 |
from session: ChargeSessionSummary, |
| 316 | 319 |
replacingLiveBufferIfNeeded: Bool = false |
| 317 |
- ) {
|
|
| 320 |
+ ) -> Bool {
|
|
| 318 | 321 |
let hasExistingBuffer = |
| 319 | 322 |
power.points.isEmpty == false || |
| 320 | 323 |
voltage.points.isEmpty == false || |
@@ -323,8 +326,13 @@ class Measurements : ObservableObject {
|
||
| 323 | 326 |
energy.points.isEmpty == false || |
| 324 | 327 |
rssi.points.isEmpty == false |
| 325 | 328 |
|
| 329 |
+ restoreTrace( |
|
| 330 |
+ "measurements-restore-start session=\(session.id.uuidString) status=\(session.status.rawValue) persistedSamples=\(session.aggregatedSamples.count) replaceLive=\(replacingLiveBufferIfNeeded) existingBuffer=\(hasExistingBuffer) existingCounts=p:\(power.points.count),v:\(voltage.points.count),c:\(current.points.count),t:\(temperature.points.count),e:\(energy.points.count),r:\(rssi.points.count)" |
|
| 331 |
+ ) |
|
| 332 |
+ |
|
| 326 | 333 |
guard hasExistingBuffer == false || replacingLiveBufferIfNeeded else {
|
| 327 |
- return |
|
| 334 |
+ restoreTrace("measurements-restore-skip session=\(session.id.uuidString) reason=live-buffer-not-replaced")
|
|
| 335 |
+ return false |
|
| 328 | 336 |
} |
| 329 | 337 |
|
| 330 | 338 |
let sortedSamples = session.aggregatedSamples.sorted { lhs, rhs in
|
@@ -334,7 +342,10 @@ class Measurements : ObservableObject {
|
||
| 334 | 342 |
return lhs.timestamp < rhs.timestamp |
| 335 | 343 |
} |
| 336 | 344 |
|
| 337 |
- guard !sortedSamples.isEmpty else { return }
|
|
| 345 |
+ guard !sortedSamples.isEmpty else {
|
|
| 346 |
+ restoreTrace("measurements-restore-skip session=\(session.id.uuidString) reason=no-persisted-samples")
|
|
| 347 |
+ return false |
|
| 348 |
+ } |
|
| 338 | 349 |
|
| 339 | 350 |
let preservedEnergyCounterValue = lastEnergyCounterValue |
| 340 | 351 |
let preservedEnergyGroupID = lastEnergyGroupID |
@@ -345,50 +356,67 @@ class Measurements : ObservableObject {
|
||
| 345 | 356 |
|
| 346 | 357 |
resetPendingAggregation() |
| 347 | 358 |
|
| 348 |
- power.replacePoints(mergedRestoredPoints( |
|
| 349 |
- restored: restoredPoints(from: sortedSamples) { sample in
|
|
| 350 |
- sample.averagePowerWatts |
|
| 351 |
- }, |
|
| 359 |
+ let restoredPowerPoints = restoredPoints(from: sortedSamples) { sample in
|
|
| 360 |
+ sample.averagePowerWatts |
|
| 361 |
+ } |
|
| 362 |
+ let restoredCurrentPoints = restoredPoints(from: sortedSamples) { sample in
|
|
| 363 |
+ sample.averageCurrentAmps |
|
| 364 |
+ } |
|
| 365 |
+ let restoredVoltagePoints = restoredPoints(from: sortedSamples) { sample in
|
|
| 366 |
+ sample.averageVoltageVolts |
|
| 367 |
+ } |
|
| 368 |
+ let restoredEnergyPoints = restoredPoints(from: sortedSamples) { sample in
|
|
| 369 |
+ sample.measuredEnergyWh |
|
| 370 |
+ } |
|
| 371 |
+ |
|
| 372 |
+ let mergedPowerPoints = mergedRestoredPoints( |
|
| 373 |
+ restored: restoredPowerPoints, |
|
| 352 | 374 |
existing: power.points, |
| 353 | 375 |
persistedRangeUpperBound: persistedRangeUpperBound |
| 354 |
- )) |
|
| 355 |
- current.replacePoints(mergedRestoredPoints( |
|
| 356 |
- restored: restoredPoints(from: sortedSamples) { sample in
|
|
| 357 |
- sample.averageCurrentAmps |
|
| 358 |
- }, |
|
| 376 |
+ ) |
|
| 377 |
+ let mergedCurrentPoints = mergedRestoredPoints( |
|
| 378 |
+ restored: restoredCurrentPoints, |
|
| 359 | 379 |
existing: current.points, |
| 360 | 380 |
persistedRangeUpperBound: persistedRangeUpperBound |
| 361 |
- )) |
|
| 362 |
- voltage.replacePoints(mergedRestoredPoints( |
|
| 363 |
- restored: restoredPoints(from: sortedSamples) { sample in
|
|
| 364 |
- sample.averageVoltageVolts |
|
| 365 |
- }, |
|
| 381 |
+ ) |
|
| 382 |
+ let mergedVoltagePoints = mergedRestoredPoints( |
|
| 383 |
+ restored: restoredVoltagePoints, |
|
| 366 | 384 |
existing: voltage.points, |
| 367 | 385 |
persistedRangeUpperBound: persistedRangeUpperBound |
| 368 |
- )) |
|
| 369 |
- energy.replacePoints(mergedRestoredPoints( |
|
| 370 |
- restored: restoredPoints(from: sortedSamples) { sample in
|
|
| 371 |
- sample.measuredEnergyWh |
|
| 372 |
- }, |
|
| 386 |
+ ) |
|
| 387 |
+ let mergedEnergyPoints = mergedRestoredPoints( |
|
| 388 |
+ restored: restoredEnergyPoints, |
|
| 373 | 389 |
existing: energy.points, |
| 374 | 390 |
persistedRangeUpperBound: persistedRangeUpperBound |
| 375 |
- )) |
|
| 376 |
- temperature.replacePoints( |
|
| 377 |
- preservedTailPoints( |
|
| 378 |
- from: temperature.points, |
|
| 379 |
- after: persistedRangeUpperBound |
|
| 380 |
- ) |
|
| 381 | 391 |
) |
| 382 |
- rssi.replacePoints( |
|
| 383 |
- preservedTailPoints( |
|
| 384 |
- from: rssi.points, |
|
| 385 |
- after: persistedRangeUpperBound |
|
| 386 |
- ) |
|
| 392 |
+ let preservedTemperatureTail = preservedTailPoints( |
|
| 393 |
+ from: temperature.points, |
|
| 394 |
+ after: persistedRangeUpperBound |
|
| 395 |
+ ) |
|
| 396 |
+ let preservedRssiTail = preservedTailPoints( |
|
| 397 |
+ from: rssi.points, |
|
| 398 |
+ after: persistedRangeUpperBound |
|
| 387 | 399 |
) |
| 400 |
+ |
|
| 401 |
+ restoreTrace( |
|
| 402 |
+ "measurements-restore-merge session=\(session.id.uuidString) restored=p:\(restoredPowerPoints.count),v:\(restoredVoltagePoints.count),c:\(restoredCurrentPoints.count),e:\(restoredEnergyPoints.count) discontinuities=p:\(restoredPowerPoints.filter(\.isDiscontinuity).count),v:\(restoredVoltagePoints.filter(\.isDiscontinuity).count),c:\(restoredCurrentPoints.filter(\.isDiscontinuity).count),e:\(restoredEnergyPoints.filter(\.isDiscontinuity).count) merged=p:\(mergedPowerPoints.count),v:\(mergedVoltagePoints.count),c:\(mergedCurrentPoints.count),e:\(mergedEnergyPoints.count) tails=t:\(preservedTemperatureTail.count),r:\(preservedRssiTail.count) upperBound=\(persistedRangeUpperBound?.description ?? "nil")" |
|
| 403 |
+ ) |
|
| 404 |
+ |
|
| 405 |
+ power.replacePoints(mergedPowerPoints) |
|
| 406 |
+ current.replacePoints(mergedCurrentPoints) |
|
| 407 |
+ voltage.replacePoints(mergedVoltagePoints) |
|
| 408 |
+ energy.replacePoints(mergedEnergyPoints) |
|
| 409 |
+ temperature.replacePoints(preservedTemperatureTail) |
|
| 410 |
+ rssi.replacePoints(preservedRssiTail) |
|
| 411 |
+ |
|
| 388 | 412 |
lastEnergyCounterValue = hasExistingBuffer ? preservedEnergyCounterValue : nil |
| 389 | 413 |
lastEnergyGroupID = hasExistingBuffer ? preservedEnergyGroupID : nil |
| 390 | 414 |
accumulatedEnergyValue = energy.samplePoints.last?.value ?? 0 |
| 391 | 415 |
self.objectWillChange.send() |
| 416 |
+ restoreTrace( |
|
| 417 |
+ "measurements-restore-complete session=\(session.id.uuidString) counts=p:\(power.samplePoints.count),v:\(voltage.samplePoints.count),c:\(current.samplePoints.count),t:\(temperature.samplePoints.count),e:\(energy.samplePoints.count),r:\(rssi.samplePoints.count) accumulatedEnergy=\(accumulatedEnergyValue)" |
|
| 418 |
+ ) |
|
| 419 |
+ return true |
|
| 392 | 420 |
} |
| 393 | 421 |
|
| 394 | 422 |
private func restoredPoints( |
@@ -402,7 +430,7 @@ class Measurements : ObservableObject {
|
||
| 402 | 430 |
guard let pointValue = value(sample) else { continue }
|
| 403 | 431 |
|
| 404 | 432 |
if let previousSample, |
| 405 |
- sample.bucketIndex - previousSample.bucketIndex > 1 {
|
|
| 433 |
+ sample.timestamp.timeIntervalSince(previousSample.timestamp) > Self.restoredSampleDiscontinuityThreshold {
|
|
| 406 | 434 |
restored.append( |
| 407 | 435 |
Measurement.Point( |
| 408 | 436 |
id: restored.count, |
@@ -976,7 +976,10 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 976 | 976 |
resetChargeRecord() |
| 977 | 977 |
} |
| 978 | 978 |
|
| 979 |
- func restoreChargeRecordIfNeeded(from activeSession: ChargeSessionSummary) {
|
|
| 979 |
+ func restoreChargeRecordIfNeeded( |
|
| 980 |
+ from activeSession: ChargeSessionSummary, |
|
| 981 |
+ replacingLiveBufferIfNeeded: Bool = false |
|
| 982 |
+ ) {
|
|
| 980 | 983 |
var didChange = false |
| 981 | 984 |
let restoreSignature = ChargeRecordRestoreSignature( |
| 982 | 985 |
sessionID: activeSession.id, |
@@ -985,11 +988,16 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 985 | 988 |
) |
| 986 | 989 |
|
| 987 | 990 |
if restoreSignature != restoredChargeRecordSignature {
|
| 988 |
- chargeRecordMeasurements.restorePersistedChargeSessionSamplesIfNeeded( |
|
| 989 |
- from: activeSession |
|
| 991 |
+ restoreTrace("meter=\(name) charge-record-restore-start session=\(activeSession.id.uuidString) status=\(activeSession.status.rawValue) samples=\(restoreSignature.sampleCount) lastTs=\(restoreSignature.lastSampleTimestamp?.description ?? "nil") replaceLive=\(replacingLiveBufferIfNeeded) state=\(chargeRecordState) existingPower=\(chargeRecordMeasurements.power.samplePoints.count)")
|
|
| 992 |
+ let didRestorePersistedSamples = chargeRecordMeasurements.restorePersistedChargeSessionSamplesIfNeeded( |
|
| 993 |
+ from: activeSession, |
|
| 994 |
+ replacingLiveBufferIfNeeded: replacingLiveBufferIfNeeded |
|
| 990 | 995 |
) |
| 991 |
- if activeSession.aggregatedSamples.isEmpty == false {
|
|
| 996 |
+ restoreTrace("meter=\(name) charge-record-restore-result session=\(activeSession.id.uuidString) didRestore=\(didRestorePersistedSamples) priorSignatureSamples=\(restoredChargeRecordSignature?.sampleCount.description ?? "nil")")
|
|
| 997 |
+ if didRestorePersistedSamples || activeSession.aggregatedSamples.isEmpty == false {
|
|
| 992 | 998 |
restoredChargeRecordSignature = restoreSignature |
| 999 |
+ } |
|
| 1000 |
+ if didRestorePersistedSamples {
|
|
| 993 | 1001 |
didChange = true |
| 994 | 1002 |
} |
| 995 | 1003 |
} |
@@ -1082,13 +1090,26 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 1082 | 1090 |
} |
| 1083 | 1091 |
|
| 1084 | 1092 |
func restoreChargeMonitoringIfNeeded(from activeSession: ChargeSessionSummary) {
|
| 1085 |
- restoreChargeRecordIfNeeded(from: activeSession) |
|
| 1093 |
+ let shouldReplaceLiveBuffer = restoredChargeSessionID != activeSession.id |
|
| 1094 |
+ if shouldReplaceLiveBuffer {
|
|
| 1095 |
+ restoreTrace("meter=\(name) charge-monitoring-restore-request session=\(activeSession.id.uuidString) status=\(activeSession.status.rawValue) replaceLive=\(shouldReplaceLiveBuffer) restoredSession=\(restoredChargeSessionID?.uuidString ?? "nil")")
|
|
| 1096 |
+ } |
|
| 1097 |
+ restoreChargeRecordIfNeeded( |
|
| 1098 |
+ from: activeSession, |
|
| 1099 |
+ replacingLiveBufferIfNeeded: shouldReplaceLiveBuffer |
|
| 1100 |
+ ) |
|
| 1086 | 1101 |
|
| 1087 | 1102 |
guard restoredChargeSessionID != activeSession.id else {
|
| 1088 | 1103 |
return |
| 1089 | 1104 |
} |
| 1090 | 1105 |
|
| 1091 | 1106 |
restoredChargeSessionID = activeSession.id |
| 1107 |
+ |
|
| 1108 |
+ guard activeSession.status == .active else {
|
|
| 1109 |
+ restoreTrace("meter=\(name) charge-monitoring-restore-no-reconnect session=\(activeSession.id.uuidString) status=\(activeSession.status.rawValue)")
|
|
| 1110 |
+ return |
|
| 1111 |
+ } |
|
| 1112 |
+ |
|
| 1092 | 1113 |
enableAutoConnect = true |
| 1093 | 1114 |
|
| 1094 | 1115 |
guard operationalState < .peripheralConnectionPending else {
|
@@ -204,9 +204,9 @@ struct MeterChargeRecordContentView: View {
|
||
| 204 | 204 |
|
| 205 | 205 |
private func syncActiveSessionRestore() {
|
| 206 | 206 |
guard let session = openChargeSession else { return }
|
| 207 |
- guard session.status == .active else { return }
|
|
| 207 |
+ guard session.status.isOpen else { return }
|
|
| 208 | 208 |
guard session.meterMACAddress == meterMACAddress else { return }
|
| 209 |
- usbMeter.restoreChargeRecordIfNeeded(from: session) |
|
| 209 |
+ usbMeter.restoreChargeMonitoringIfNeeded(from: session) |
|
| 210 | 210 |
} |
| 211 | 211 |
|
| 212 | 212 |
private func runTrimDetection() {
|