// // Measurements.swift // USB Meter // // Created by Bogdan Timofte on 07/05/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import Foundation import CoreGraphics class Measurements : ObservableObject { class Measurement : ObservableObject { struct Point : Identifiable , Hashable { enum Kind: Hashable { case sample case discontinuity } var id : Int var timestamp: Date var value: Double var kind: Kind = .sample var isSample: Bool { kind == .sample } var isDiscontinuity: Bool { kind == .discontinuity } func point() -> CGPoint { return CGPoint(x: timestamp.timeIntervalSince1970, y: value) } } var points: [Point] = [] var context = ChartContext() var samplePoints: [Point] { points.filter { $0.isSample } } private func rebuildContext() { context.reset() for point in points where point.isSample { context.include(point: point.point()) } } private func appendPoint(timestamp: Date, value: Double, kind: Point.Kind) { let newPoint = Measurement.Point(id: points.count, timestamp: timestamp, value: value, kind: kind) points.append(newPoint) if newPoint.isSample { context.include(point: newPoint.point()) } self.objectWillChange.send() } func removeValue(index: Int) { points.remove(at: index) for index in points.indices { points[index].id = index } rebuildContext() self.objectWillChange.send() } func addPoint(timestamp: Date, value: Double) { appendPoint(timestamp: timestamp, value: value, kind: .sample) } func addDiscontinuity(timestamp: Date) { guard !points.isEmpty else { return } guard points.last?.isDiscontinuity == false else { return } appendPoint(timestamp: timestamp, value: points.last?.value ?? 0, kind: .discontinuity) } func resetSeries() { points.removeAll() context.reset() self.objectWillChange.send() } func trim(before cutoff: Date) { points = points .filter { $0.timestamp >= cutoff } .enumerated() .map { index, point in Measurement.Point(id: index, timestamp: point.timestamp, value: point.value, kind: point.kind) } rebuildContext() self.objectWillChange.send() } } @Published var power = Measurement() @Published var voltage = Measurement() @Published var current = Measurement() @Published var temperature = Measurement() @Published var rssi = Measurement() let averagePowerSampleOptions: [Int] = [5, 10, 20, 50, 100, 250] private var pendingBucketSecond: Int? private var pendingBucketTimestamp: Date? private var itemsInSum: Double = 0 private var powerSum: Double = 0 private var voltageSum: Double = 0 private var currentSum: Double = 0 private var temperatureSum: Double = 0 private var rssiSum: Double = 0 private func resetPendingAggregation() { pendingBucketSecond = nil pendingBucketTimestamp = nil itemsInSum = 0 powerSum = 0 voltageSum = 0 currentSum = 0 temperatureSum = 0 rssiSum = 0 } private func flushPendingValues() { guard let pendingBucketTimestamp, itemsInSum > 0 else { return } self.power.addPoint(timestamp: pendingBucketTimestamp, value: powerSum / itemsInSum) self.voltage.addPoint(timestamp: pendingBucketTimestamp, value: voltageSum / itemsInSum) self.current.addPoint(timestamp: pendingBucketTimestamp, value: currentSum / itemsInSum) self.temperature.addPoint(timestamp: pendingBucketTimestamp, value: temperatureSum / itemsInSum) self.rssi.addPoint(timestamp: pendingBucketTimestamp, value: rssiSum / itemsInSum) resetPendingAggregation() self.objectWillChange.send() } func resetSeries() { power.resetSeries() voltage.resetSeries() current.resetSeries() temperature.resetSeries() rssi.resetSeries() resetPendingAggregation() self.objectWillChange.send() } func reset() { resetSeries() } func remove(at idx: Int) { power.removeValue(index: idx) voltage.removeValue(index: idx) current.removeValue(index: idx) temperature.removeValue(index: idx) rssi.removeValue(index: idx) self.objectWillChange.send() } func trim(before cutoff: Date) { flushPendingValues() power.trim(before: cutoff) voltage.trim(before: cutoff) current.trim(before: cutoff) temperature.trim(before: cutoff) rssi.trim(before: cutoff) self.objectWillChange.send() } func addValues(timestamp: Date, power: Double, voltage: Double, current: Double, temperature: Double, rssi: Double) { let valuesTimestamp = timestamp.timeIntervalSinceReferenceDate.intValue if pendingBucketSecond == valuesTimestamp { pendingBucketTimestamp = timestamp itemsInSum += 1 powerSum += power voltageSum += voltage currentSum += current temperatureSum += temperature rssiSum += rssi return } flushPendingValues() pendingBucketSecond = valuesTimestamp pendingBucketTimestamp = timestamp itemsInSum = 1 powerSum = power voltageSum = voltage currentSum = current temperatureSum = temperature rssiSum = rssi } func markDiscontinuity(at timestamp: Date) { flushPendingValues() power.addDiscontinuity(timestamp: timestamp) voltage.addDiscontinuity(timestamp: timestamp) current.addDiscontinuity(timestamp: timestamp) temperature.addDiscontinuity(timestamp: timestamp) rssi.addDiscontinuity(timestamp: timestamp) self.objectWillChange.send() } func powerSampleCount(flushPendingValues shouldFlushPendingValues: Bool = true) -> Int { if shouldFlushPendingValues { flushPendingValues() } return power.samplePoints.count } func recentPowerPoints(limit: Int, flushPendingValues shouldFlushPendingValues: Bool = true) -> [Measurement.Point] { if shouldFlushPendingValues { flushPendingValues() } let samplePoints = power.samplePoints guard limit > 0, samplePoints.count > limit else { return samplePoints } return Array(samplePoints.suffix(limit)) } func averagePower(forRecentSampleCount sampleCount: Int, flushPendingValues shouldFlushPendingValues: Bool = true) -> Double? { let points = recentPowerPoints(limit: sampleCount, flushPendingValues: shouldFlushPendingValues) guard !points.isEmpty else { return nil } let sum = points.reduce(0) { partialResult, point in partialResult + point.value } return sum / Double(points.count) } }