Showing 5 changed files with 101 additions and 42 deletions
+6 -0
USB Meter/AppDelegate.swift
@@ -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" {
+5 -1
USB Meter/Model/AppData.swift
@@ -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)
+62 -34
USB Meter/Model/Measurements.swift
@@ -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,
+26 -5
USB Meter/Model/Meter.swift
@@ -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 {
+2 -2
USB Meter/Views/Meter/Tabs/ChargeRecord/MeterChargeRecordTabView.swift
@@ -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() {