USB-Meter / USB Meter / Model / Measurements.swift
1 contributor
238 lines | 7.578kb
//
//  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)
    }
}