USB-Meter / USB Meter / Model / Measurements.swift
Newer Older
238 lines | 7.578kb
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

            
46
        private func rebuildContext() {
47
            context.reset()
48
            for point in points where point.isSample {
49
                context.include(point: point.point())
50
            }
51
        }
52

            
53
        private func appendPoint(timestamp: Date, value: Double, kind: Point.Kind) {
54
            let newPoint = Measurement.Point(id: points.count, timestamp: timestamp, value: value, kind: kind)
55
            points.append(newPoint)
56
            if newPoint.isSample {
57
                context.include(point: newPoint.point())
58
            }
59
            self.objectWillChange.send()
60
        }
61

            
Bogdan Timofte authored 2 weeks ago
62
        func removeValue(index: Int) {
63
            points.remove(at: index)
Bogdan Timofte authored a week ago
64
            for index in points.indices {
65
                points[index].id = index
Bogdan Timofte authored 2 weeks ago
66
            }
Bogdan Timofte authored a week ago
67
            rebuildContext()
Bogdan Timofte authored 2 weeks ago
68
            self.objectWillChange.send()
69
        }
70

            
71
        func addPoint(timestamp: Date, value: Double) {
Bogdan Timofte authored a week ago
72
            appendPoint(timestamp: timestamp, value: value, kind: .sample)
73
        }
74

            
75
        func addDiscontinuity(timestamp: Date) {
76
            guard !points.isEmpty else { return }
77
            guard points.last?.isDiscontinuity == false else { return }
78
            appendPoint(timestamp: timestamp, value: points.last?.value ?? 0, kind: .discontinuity)
Bogdan Timofte authored 2 weeks ago
79
        }
80

            
Bogdan Timofte authored a week ago
81
        func resetSeries() {
Bogdan Timofte authored 2 weeks ago
82
            points.removeAll()
83
            context.reset()
84
            self.objectWillChange.send()
85
        }
Bogdan Timofte authored 2 weeks ago
86

            
87
        func trim(before cutoff: Date) {
88
            points = points
89
                .filter { $0.timestamp >= cutoff }
90
                .enumerated()
91
                .map { index, point in
Bogdan Timofte authored a week ago
92
                    Measurement.Point(id: index, timestamp: point.timestamp, value: point.value, kind: point.kind)
Bogdan Timofte authored 2 weeks ago
93
                }
Bogdan Timofte authored a week ago
94
            rebuildContext()
Bogdan Timofte authored 2 weeks ago
95
            self.objectWillChange.send()
96
        }
Bogdan Timofte authored 2 weeks ago
97
    }
98

            
99
    @Published var power = Measurement()
100
    @Published var voltage = Measurement()
101
    @Published var current = Measurement()
Bogdan Timofte authored 4 days ago
102
    @Published var temperature = Measurement()
103
    @Published var rssi = Measurement()
104

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

            
Bogdan Timofte authored a week ago
107
    private var pendingBucketSecond: Int?
108
    private var pendingBucketTimestamp: Date?
Bogdan Timofte authored 2 weeks ago
109

            
110
    private var itemsInSum: Double = 0
111
    private var powerSum: Double = 0
112
    private var voltageSum: Double = 0
113
    private var currentSum: Double = 0
Bogdan Timofte authored 4 days ago
114
    private var temperatureSum: Double = 0
115
    private var rssiSum: Double = 0
Bogdan Timofte authored 2 weeks ago
116

            
Bogdan Timofte authored a week ago
117
    private func resetPendingAggregation() {
118
        pendingBucketSecond = nil
119
        pendingBucketTimestamp = nil
Bogdan Timofte authored 2 weeks ago
120
        itemsInSum = 0
121
        powerSum = 0
122
        voltageSum = 0
123
        currentSum = 0
Bogdan Timofte authored 4 days ago
124
        temperatureSum = 0
125
        rssiSum = 0
Bogdan Timofte authored a week ago
126
    }
127

            
128
    private func flushPendingValues() {
129
        guard let pendingBucketTimestamp, itemsInSum > 0 else { return }
130
        self.power.addPoint(timestamp: pendingBucketTimestamp, value: powerSum / itemsInSum)
131
        self.voltage.addPoint(timestamp: pendingBucketTimestamp, value: voltageSum / itemsInSum)
132
        self.current.addPoint(timestamp: pendingBucketTimestamp, value: currentSum / itemsInSum)
Bogdan Timofte authored 4 days ago
133
        self.temperature.addPoint(timestamp: pendingBucketTimestamp, value: temperatureSum / itemsInSum)
134
        self.rssi.addPoint(timestamp: pendingBucketTimestamp, value: rssiSum / itemsInSum)
Bogdan Timofte authored a week ago
135
        resetPendingAggregation()
Bogdan Timofte authored 2 weeks ago
136
        self.objectWillChange.send()
137
    }
Bogdan Timofte authored a week ago
138

            
139
    func resetSeries() {
140
        power.resetSeries()
141
        voltage.resetSeries()
142
        current.resetSeries()
Bogdan Timofte authored 4 days ago
143
        temperature.resetSeries()
144
        rssi.resetSeries()
Bogdan Timofte authored a week ago
145
        resetPendingAggregation()
146
        self.objectWillChange.send()
147
    }
148

            
149
    func reset() {
150
        resetSeries()
151
    }
Bogdan Timofte authored 2 weeks ago
152

            
153
    func remove(at idx: Int) {
154
        power.removeValue(index: idx)
155
        voltage.removeValue(index: idx)
156
        current.removeValue(index: idx)
Bogdan Timofte authored 4 days ago
157
        temperature.removeValue(index: idx)
158
        rssi.removeValue(index: idx)
Bogdan Timofte authored 2 weeks ago
159
        self.objectWillChange.send()
160
    }
161

            
Bogdan Timofte authored 2 weeks ago
162
    func trim(before cutoff: Date) {
Bogdan Timofte authored a week ago
163
        flushPendingValues()
Bogdan Timofte authored 2 weeks ago
164
        power.trim(before: cutoff)
165
        voltage.trim(before: cutoff)
166
        current.trim(before: cutoff)
Bogdan Timofte authored 4 days ago
167
        temperature.trim(before: cutoff)
168
        rssi.trim(before: cutoff)
Bogdan Timofte authored 2 weeks ago
169
        self.objectWillChange.send()
170
    }
171

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

            
175
        if pendingBucketSecond == valuesTimestamp {
176
            pendingBucketTimestamp = timestamp
Bogdan Timofte authored 2 weeks ago
177
            itemsInSum += 1
Bogdan Timofte authored 2 weeks ago
178
            powerSum += power
Bogdan Timofte authored 2 weeks ago
179
            voltageSum += voltage
180
            currentSum += current
Bogdan Timofte authored 4 days ago
181
            temperatureSum += temperature
182
            rssiSum += rssi
Bogdan Timofte authored a week ago
183
            return
Bogdan Timofte authored 2 weeks ago
184
        }
Bogdan Timofte authored a week ago
185

            
186
        flushPendingValues()
187

            
188
        pendingBucketSecond = valuesTimestamp
189
        pendingBucketTimestamp = timestamp
190
        itemsInSum = 1
191
        powerSum = power
192
        voltageSum = voltage
193
        currentSum = current
Bogdan Timofte authored 4 days ago
194
        temperatureSum = temperature
195
        rssiSum = rssi
Bogdan Timofte authored a week ago
196
    }
197

            
198
    func markDiscontinuity(at timestamp: Date) {
199
        flushPendingValues()
200
        power.addDiscontinuity(timestamp: timestamp)
201
        voltage.addDiscontinuity(timestamp: timestamp)
202
        current.addDiscontinuity(timestamp: timestamp)
Bogdan Timofte authored 4 days ago
203
        temperature.addDiscontinuity(timestamp: timestamp)
204
        rssi.addDiscontinuity(timestamp: timestamp)
Bogdan Timofte authored a week ago
205
        self.objectWillChange.send()
Bogdan Timofte authored 2 weeks ago
206
    }
Bogdan Timofte authored 4 days ago
207

            
208
    func powerSampleCount(flushPendingValues shouldFlushPendingValues: Bool = true) -> Int {
209
        if shouldFlushPendingValues {
210
            flushPendingValues()
211
        }
212
        return power.samplePoints.count
213
    }
214

            
215
    func recentPowerPoints(limit: Int, flushPendingValues shouldFlushPendingValues: Bool = true) -> [Measurement.Point] {
216
        if shouldFlushPendingValues {
217
            flushPendingValues()
218
        }
219

            
220
        let samplePoints = power.samplePoints
221
        guard limit > 0, samplePoints.count > limit else {
222
            return samplePoints
223
        }
224

            
225
        return Array(samplePoints.suffix(limit))
226
    }
227

            
228
    func averagePower(forRecentSampleCount sampleCount: Int, flushPendingValues shouldFlushPendingValues: Bool = true) -> Double? {
229
        let points = recentPowerPoints(limit: sampleCount, flushPendingValues: shouldFlushPendingValues)
230
        guard !points.isEmpty else { return nil }
231

            
232
        let sum = points.reduce(0) { partialResult, point in
233
            partialResult + point.value
234
        }
235

            
236
        return sum / Double(points.count)
237
    }
Bogdan Timofte authored 2 weeks ago
238
}