USB-Meter / USB Meter / Model / Measurements.swift
Newer Older
c34c6eb 19 hours ago History
385 lines | 13.255kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  Measurements.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 07/05/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
9
import Foundation
10
import CoreGraphics
11

            
12
class Measurements : ObservableObject {
13

            
14
    class Measurement : ObservableObject {
15
        struct Point : Identifiable , Hashable {
Bogdan Timofte authored a week ago
16
            enum Kind: Hashable {
17
                case sample
18
                case discontinuity
19
            }
20

            
Bogdan Timofte authored 2 weeks ago
21
            var id : Int
22
            var timestamp: Date
23
            var value: Double
Bogdan Timofte authored a week ago
24
            var kind: Kind = .sample
25

            
26
            var isSample: Bool {
27
                kind == .sample
28
            }
29

            
30
            var isDiscontinuity: Bool {
31
                kind == .discontinuity
32
            }
33

            
Bogdan Timofte authored 2 weeks ago
34
            func point() -> CGPoint {
35
                return CGPoint(x: timestamp.timeIntervalSince1970, y: value)
36
            }
37
        }
38

            
39
        var points: [Point] = []
40
        var context = ChartContext()
41

            
Bogdan Timofte authored a week ago
42
        var samplePoints: [Point] {
43
            points.filter { $0.isSample }
44
        }
45

            
Bogdan Timofte authored 19 hours ago
46
        func points(in range: ClosedRange<Date>) -> [Point] {
47
            guard !points.isEmpty else { return [] }
48

            
49
            let startIndex = indexOfFirstPoint(onOrAfter: range.lowerBound)
50
            let endIndex = indexOfFirstPoint(after: range.upperBound)
51
            guard startIndex < endIndex else { return [] }
52
            return Array(points[startIndex..<endIndex])
53
        }
54

            
Bogdan Timofte authored a week ago
55
        private func rebuildContext() {
56
            context.reset()
57
            for point in points where point.isSample {
58
                context.include(point: point.point())
59
            }
60
        }
61

            
62
        private func appendPoint(timestamp: Date, value: Double, kind: Point.Kind) {
63
            let newPoint = Measurement.Point(id: points.count, timestamp: timestamp, value: value, kind: kind)
64
            points.append(newPoint)
65
            if newPoint.isSample {
66
                context.include(point: newPoint.point())
67
            }
68
            self.objectWillChange.send()
69
        }
70

            
Bogdan Timofte authored 2 weeks ago
71
        func removeValue(index: Int) {
Bogdan Timofte authored 19 hours ago
72
            guard points.indices.contains(index) else { return }
Bogdan Timofte authored 2 weeks ago
73
            points.remove(at: index)
Bogdan Timofte authored a week ago
74
            for index in points.indices {
75
                points[index].id = index
Bogdan Timofte authored 2 weeks ago
76
            }
Bogdan Timofte authored a week ago
77
            rebuildContext()
Bogdan Timofte authored 2 weeks ago
78
            self.objectWillChange.send()
79
        }
80

            
81
        func addPoint(timestamp: Date, value: Double) {
Bogdan Timofte authored a week ago
82
            appendPoint(timestamp: timestamp, value: value, kind: .sample)
83
        }
84

            
85
        func addDiscontinuity(timestamp: Date) {
86
            guard !points.isEmpty else { return }
87
            guard points.last?.isDiscontinuity == false else { return }
88
            appendPoint(timestamp: timestamp, value: points.last?.value ?? 0, kind: .discontinuity)
Bogdan Timofte authored 2 weeks ago
89
        }
90

            
Bogdan Timofte authored a week ago
91
        func resetSeries() {
Bogdan Timofte authored 2 weeks ago
92
            points.removeAll()
93
            context.reset()
94
            self.objectWillChange.send()
95
        }
Bogdan Timofte authored 2 weeks ago
96

            
97
        func trim(before cutoff: Date) {
98
            points = points
99
                .filter { $0.timestamp >= cutoff }
100
                .enumerated()
101
                .map { index, point in
Bogdan Timofte authored a week ago
102
                    Measurement.Point(id: index, timestamp: point.timestamp, value: point.value, kind: point.kind)
Bogdan Timofte authored 2 weeks ago
103
                }
Bogdan Timofte authored a week ago
104
            rebuildContext()
Bogdan Timofte authored 2 weeks ago
105
            self.objectWillChange.send()
106
        }
Bogdan Timofte authored 19 hours ago
107

            
108
        func filterSamples(keeping shouldKeepSampleAt: (Date) -> Bool) {
109
            let originalSamples = samplePoints
110
            guard !originalSamples.isEmpty else { return }
111

            
112
            var rebuiltPoints: [Point] = []
113
            var lastKeptSampleIndex: Int?
114

            
115
            for (sampleIndex, sample) in originalSamples.enumerated() where shouldKeepSampleAt(sample.timestamp) {
116
                if let lastKeptSampleIndex {
117
                    let hasRemovedSamplesBetween = sampleIndex - lastKeptSampleIndex > 1
118
                    let previousSample = originalSamples[lastKeptSampleIndex]
119
                    let originalHadDiscontinuityBetween = points.contains { point in
120
                        point.isDiscontinuity &&
121
                        point.timestamp > previousSample.timestamp &&
122
                        point.timestamp <= sample.timestamp
123
                    }
124

            
125
                    if hasRemovedSamplesBetween || originalHadDiscontinuityBetween {
126
                        rebuiltPoints.append(
127
                            Point(
128
                                id: rebuiltPoints.count,
129
                                timestamp: sample.timestamp,
130
                                value: rebuiltPoints.last?.value ?? sample.value,
131
                                kind: .discontinuity
132
                            )
133
                        )
134
                    }
135
                }
136

            
137
                rebuiltPoints.append(
138
                    Point(
139
                        id: rebuiltPoints.count,
140
                        timestamp: sample.timestamp,
141
                        value: sample.value,
142
                        kind: .sample
143
                    )
144
                )
145
                lastKeptSampleIndex = sampleIndex
146
            }
147

            
148
            points = rebuiltPoints
149
            rebuildContext()
150
            self.objectWillChange.send()
151
        }
152

            
153
        private func indexOfFirstPoint(onOrAfter date: Date) -> Int {
154
            var lowerBound = 0
155
            var upperBound = points.count
156

            
157
            while lowerBound < upperBound {
158
                let midIndex = (lowerBound + upperBound) / 2
159
                if points[midIndex].timestamp < date {
160
                    lowerBound = midIndex + 1
161
                } else {
162
                    upperBound = midIndex
163
                }
164
            }
165

            
166
            return lowerBound
167
        }
168

            
169
        private func indexOfFirstPoint(after date: Date) -> Int {
170
            var lowerBound = 0
171
            var upperBound = points.count
172

            
173
            while lowerBound < upperBound {
174
                let midIndex = (lowerBound + upperBound) / 2
175
                if points[midIndex].timestamp <= date {
176
                    lowerBound = midIndex + 1
177
                } else {
178
                    upperBound = midIndex
179
                }
180
            }
181

            
182
            return lowerBound
183
        }
Bogdan Timofte authored 2 weeks ago
184
    }
185

            
186
    @Published var power = Measurement()
187
    @Published var voltage = Measurement()
188
    @Published var current = Measurement()
Bogdan Timofte authored 4 days ago
189
    @Published var temperature = Measurement()
Bogdan Timofte authored 19 hours ago
190
    @Published var energy = Measurement()
Bogdan Timofte authored 4 days ago
191
    @Published var rssi = Measurement()
192

            
193
    let averagePowerSampleOptions: [Int] = [5, 10, 20, 50, 100, 250]
Bogdan Timofte authored 2 weeks ago
194

            
Bogdan Timofte authored a week ago
195
    private var pendingBucketSecond: Int?
196
    private var pendingBucketTimestamp: Date?
Bogdan Timofte authored 19 hours ago
197
    private let energyResetEpsilon = 0.0005
198
    private var lastEnergyCounterValue: Double?
199
    private var lastEnergyGroupID: UInt8?
200
    private var accumulatedEnergyValue: Double = 0
Bogdan Timofte authored 2 weeks ago
201

            
202
    private var itemsInSum: Double = 0
203
    private var powerSum: Double = 0
204
    private var voltageSum: Double = 0
205
    private var currentSum: Double = 0
Bogdan Timofte authored 4 days ago
206
    private var temperatureSum: Double = 0
207
    private var rssiSum: Double = 0
Bogdan Timofte authored 2 weeks ago
208

            
Bogdan Timofte authored a week ago
209
    private func resetPendingAggregation() {
210
        pendingBucketSecond = nil
211
        pendingBucketTimestamp = nil
Bogdan Timofte authored 2 weeks ago
212
        itemsInSum = 0
213
        powerSum = 0
214
        voltageSum = 0
215
        currentSum = 0
Bogdan Timofte authored 4 days ago
216
        temperatureSum = 0
217
        rssiSum = 0
Bogdan Timofte authored a week ago
218
    }
219

            
220
    private func flushPendingValues() {
221
        guard let pendingBucketTimestamp, itemsInSum > 0 else { return }
222
        self.power.addPoint(timestamp: pendingBucketTimestamp, value: powerSum / itemsInSum)
223
        self.voltage.addPoint(timestamp: pendingBucketTimestamp, value: voltageSum / itemsInSum)
224
        self.current.addPoint(timestamp: pendingBucketTimestamp, value: currentSum / itemsInSum)
Bogdan Timofte authored 4 days ago
225
        self.temperature.addPoint(timestamp: pendingBucketTimestamp, value: temperatureSum / itemsInSum)
226
        self.rssi.addPoint(timestamp: pendingBucketTimestamp, value: rssiSum / itemsInSum)
Bogdan Timofte authored a week ago
227
        resetPendingAggregation()
Bogdan Timofte authored 2 weeks ago
228
        self.objectWillChange.send()
229
    }
Bogdan Timofte authored a week ago
230

            
231
    func resetSeries() {
232
        power.resetSeries()
233
        voltage.resetSeries()
234
        current.resetSeries()
Bogdan Timofte authored 4 days ago
235
        temperature.resetSeries()
Bogdan Timofte authored 19 hours ago
236
        energy.resetSeries()
Bogdan Timofte authored 4 days ago
237
        rssi.resetSeries()
Bogdan Timofte authored a week ago
238
        resetPendingAggregation()
Bogdan Timofte authored 19 hours ago
239
        lastEnergyCounterValue = nil
240
        lastEnergyGroupID = nil
241
        accumulatedEnergyValue = 0
Bogdan Timofte authored a week ago
242
        self.objectWillChange.send()
243
    }
244

            
245
    func reset() {
246
        resetSeries()
247
    }
Bogdan Timofte authored 2 weeks ago
248

            
249
    func remove(at idx: Int) {
250
        power.removeValue(index: idx)
251
        voltage.removeValue(index: idx)
252
        current.removeValue(index: idx)
Bogdan Timofte authored 4 days ago
253
        temperature.removeValue(index: idx)
Bogdan Timofte authored 19 hours ago
254
        energy.removeValue(index: idx)
Bogdan Timofte authored 4 days ago
255
        rssi.removeValue(index: idx)
Bogdan Timofte authored 19 hours ago
256
        lastEnergyCounterValue = nil
257
        lastEnergyGroupID = nil
258
        accumulatedEnergyValue = 0
Bogdan Timofte authored 2 weeks ago
259
        self.objectWillChange.send()
260
    }
261

            
Bogdan Timofte authored 2 weeks ago
262
    func trim(before cutoff: Date) {
Bogdan Timofte authored a week ago
263
        flushPendingValues()
Bogdan Timofte authored 2 weeks ago
264
        power.trim(before: cutoff)
265
        voltage.trim(before: cutoff)
266
        current.trim(before: cutoff)
Bogdan Timofte authored 4 days ago
267
        temperature.trim(before: cutoff)
Bogdan Timofte authored 19 hours ago
268
        energy.trim(before: cutoff)
Bogdan Timofte authored 4 days ago
269
        rssi.trim(before: cutoff)
Bogdan Timofte authored 19 hours ago
270
        lastEnergyCounterValue = nil
271
        lastEnergyGroupID = nil
272
        accumulatedEnergyValue = 0
273
        self.objectWillChange.send()
274
    }
275

            
276
    func keepOnly(in range: ClosedRange<Date>) {
277
        flushPendingValues()
278
        power.filterSamples { range.contains($0) }
279
        voltage.filterSamples { range.contains($0) }
280
        current.filterSamples { range.contains($0) }
281
        temperature.filterSamples { range.contains($0) }
282
        energy.filterSamples { range.contains($0) }
283
        rssi.filterSamples { range.contains($0) }
284
        lastEnergyCounterValue = nil
285
        lastEnergyGroupID = nil
286
        accumulatedEnergyValue = energy.samplePoints.last?.value ?? 0
287
        self.objectWillChange.send()
288
    }
289

            
290
    func removeValues(in range: ClosedRange<Date>) {
291
        flushPendingValues()
292
        power.filterSamples { !range.contains($0) }
293
        voltage.filterSamples { !range.contains($0) }
294
        current.filterSamples { !range.contains($0) }
295
        temperature.filterSamples { !range.contains($0) }
296
        energy.filterSamples { !range.contains($0) }
297
        rssi.filterSamples { !range.contains($0) }
298
        lastEnergyCounterValue = nil
299
        lastEnergyGroupID = nil
300
        accumulatedEnergyValue = energy.samplePoints.last?.value ?? 0
Bogdan Timofte authored 2 weeks ago
301
        self.objectWillChange.send()
302
    }
303

            
Bogdan Timofte authored 4 days ago
304
    func addValues(timestamp: Date, power: Double, voltage: Double, current: Double, temperature: Double, rssi: Double) {
Bogdan Timofte authored 2 weeks ago
305
        let valuesTimestamp = timestamp.timeIntervalSinceReferenceDate.intValue
Bogdan Timofte authored a week ago
306

            
307
        if pendingBucketSecond == valuesTimestamp {
308
            pendingBucketTimestamp = timestamp
Bogdan Timofte authored 2 weeks ago
309
            itemsInSum += 1
Bogdan Timofte authored 2 weeks ago
310
            powerSum += power
Bogdan Timofte authored 2 weeks ago
311
            voltageSum += voltage
312
            currentSum += current
Bogdan Timofte authored 4 days ago
313
            temperatureSum += temperature
314
            rssiSum += rssi
Bogdan Timofte authored a week ago
315
            return
Bogdan Timofte authored 2 weeks ago
316
        }
Bogdan Timofte authored a week ago
317

            
318
        flushPendingValues()
319

            
320
        pendingBucketSecond = valuesTimestamp
321
        pendingBucketTimestamp = timestamp
322
        itemsInSum = 1
323
        powerSum = power
324
        voltageSum = voltage
325
        currentSum = current
Bogdan Timofte authored 4 days ago
326
        temperatureSum = temperature
327
        rssiSum = rssi
Bogdan Timofte authored a week ago
328
    }
329

            
330
    func markDiscontinuity(at timestamp: Date) {
331
        flushPendingValues()
332
        power.addDiscontinuity(timestamp: timestamp)
333
        voltage.addDiscontinuity(timestamp: timestamp)
334
        current.addDiscontinuity(timestamp: timestamp)
Bogdan Timofte authored 4 days ago
335
        temperature.addDiscontinuity(timestamp: timestamp)
Bogdan Timofte authored 19 hours ago
336
        energy.addDiscontinuity(timestamp: timestamp)
Bogdan Timofte authored 4 days ago
337
        rssi.addDiscontinuity(timestamp: timestamp)
Bogdan Timofte authored a week ago
338
        self.objectWillChange.send()
Bogdan Timofte authored 2 weeks ago
339
    }
Bogdan Timofte authored 4 days ago
340

            
Bogdan Timofte authored 19 hours ago
341
    func captureEnergyValue(timestamp: Date, value: Double, groupID: UInt8) {
342
        if let lastEnergyCounterValue, lastEnergyGroupID == groupID {
343
            let delta = value - lastEnergyCounterValue
344
            if delta > energyResetEpsilon {
345
                accumulatedEnergyValue += delta
346
            }
347
        }
348

            
349
        energy.addPoint(timestamp: timestamp, value: accumulatedEnergyValue)
350
        lastEnergyCounterValue = value
351
        lastEnergyGroupID = groupID
352
        self.objectWillChange.send()
353
    }
354

            
Bogdan Timofte authored 4 days ago
355
    func powerSampleCount(flushPendingValues shouldFlushPendingValues: Bool = true) -> Int {
356
        if shouldFlushPendingValues {
357
            flushPendingValues()
358
        }
359
        return power.samplePoints.count
360
    }
361

            
362
    func recentPowerPoints(limit: Int, flushPendingValues shouldFlushPendingValues: Bool = true) -> [Measurement.Point] {
363
        if shouldFlushPendingValues {
364
            flushPendingValues()
365
        }
366

            
367
        let samplePoints = power.samplePoints
368
        guard limit > 0, samplePoints.count > limit else {
369
            return samplePoints
370
        }
371

            
372
        return Array(samplePoints.suffix(limit))
373
    }
374

            
375
    func averagePower(forRecentSampleCount sampleCount: Int, flushPendingValues shouldFlushPendingValues: Bool = true) -> Double? {
376
        let points = recentPowerPoints(limit: sampleCount, flushPendingValues: shouldFlushPendingValues)
377
        guard !points.isEmpty else { return nil }
378

            
379
        let sum = points.reduce(0) { partialResult, point in
380
            partialResult + point.value
381
        }
382

            
383
        return sum / Double(points.count)
384
    }
Bogdan Timofte authored 2 weeks ago
385
}