Transfer view, change data flow, actors
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/*
|
||||
final class BluetoothClient: ObservableObject {
|
||||
|
||||
private let updateInterval = 3.0
|
||||
@ -9,7 +9,7 @@ final class BluetoothClient: ObservableObject {
|
||||
|
||||
private let connection = DeviceManager()
|
||||
|
||||
private let storage: TemperatureStorage
|
||||
private let storage: PersistentStorage
|
||||
|
||||
var hasInfo: Bool {
|
||||
deviceInfo != nil
|
||||
@ -21,10 +21,24 @@ final class BluetoothClient: ObservableObject {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var isUpdatingFlag = false
|
||||
|
||||
@Published
|
||||
var shouldConnect: Bool {
|
||||
didSet {
|
||||
isUpdatingFlag = true
|
||||
connection.shouldConnectIfPossible = shouldConnect
|
||||
log.info("Should connect: \(shouldConnect)")
|
||||
isUpdatingFlag = false
|
||||
}
|
||||
}
|
||||
|
||||
init(storage: TemperatureStorage, deviceInfo: DeviceInfo? = nil) {
|
||||
init(storage: PersistentStorage, shouldConnect: Bool = false, deviceInfo: DeviceInfo? = nil) {
|
||||
self.storage = storage
|
||||
self.deviceInfo = deviceInfo
|
||||
self.shouldConnect = shouldConnect
|
||||
connection.shouldConnectIfPossible = shouldConnect
|
||||
connection.delegate = self
|
||||
}
|
||||
|
||||
@ -47,7 +61,6 @@ final class BluetoothClient: ObservableObject {
|
||||
@Published
|
||||
private(set) var deviceInfo: DeviceInfo? {
|
||||
didSet {
|
||||
updateDeviceTimeIfNeeded()
|
||||
// collectRecordedData()
|
||||
if let deviceInfo, let runningTransfer {
|
||||
runningTransfer.update(info: deviceInfo)
|
||||
@ -102,6 +115,14 @@ final class BluetoothClient: ObservableObject {
|
||||
}
|
||||
|
||||
// MARK: Requests
|
||||
|
||||
func clearDeviceStorage() {
|
||||
guard let count = deviceInfo?.numberOfRecordedBytes else {
|
||||
log.info("Can't clear device data without device info")
|
||||
return
|
||||
}
|
||||
addRequest(.clearRecordingBuffer(byteCount: count))
|
||||
}
|
||||
|
||||
private func performNextRequest() {
|
||||
guard runningRequest == nil else {
|
||||
@ -136,24 +157,6 @@ final class BluetoothClient: ObservableObject {
|
||||
openRequests.append(request)
|
||||
}
|
||||
|
||||
// MARK: Device time
|
||||
|
||||
private func updateDeviceTimeIfNeeded() {
|
||||
guard let deviceInfo else {
|
||||
return
|
||||
}
|
||||
guard !deviceInfo.hasDeviceStartTimeSet || abs(deviceInfo.clockOffset) > minimumOffsetToUpdateDeviceClock else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !openRequests.contains(where: { if case .setDeviceStartTime = $0 { return true }; return false }) else {
|
||||
return
|
||||
}
|
||||
let time = deviceInfo.calculatedDeviceStartTime.seconds
|
||||
addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time))
|
||||
log.info("Setting device start time to \(time) s (correcting offset of \(Int(deviceInfo.clockOffset)) s)")
|
||||
}
|
||||
|
||||
// MARK: Data transfer
|
||||
|
||||
@discardableResult
|
||||
@ -173,8 +176,7 @@ final class BluetoothClient: ObservableObject {
|
||||
guard info.numberOfStoredMeasurements > 0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
let transfer = TemperatureDataTransfer(info: info)
|
||||
let transfer = TemperatureDataTransfer(info: info, previous: storage.lastDeviceTime)
|
||||
runningTransfer = transfer
|
||||
let next = transfer.nextRequest()
|
||||
log.info("Starting transfer")
|
||||
@ -185,9 +187,13 @@ final class BluetoothClient: ObservableObject {
|
||||
private func didReceive(data: Data, offset: Int, count: Int) {
|
||||
guard let runningTransfer else {
|
||||
log.warning("No running transfer to process device data")
|
||||
self.runningRequest = nil
|
||||
return // TODO: Start new transfer?
|
||||
}
|
||||
runningTransfer.add(data: data, offset: offset, count: count)
|
||||
guard runningTransfer.add(data: data, offset: offset, count: count) else {
|
||||
self.runningRequest = nil
|
||||
return // TODO: Start new transfer
|
||||
}
|
||||
let next = runningTransfer.nextRequest()
|
||||
addRequest(next)
|
||||
}
|
||||
@ -202,6 +208,13 @@ final class BluetoothClient: ObservableObject {
|
||||
}
|
||||
|
||||
extension BluetoothClient: DeviceManagerDelegate {
|
||||
|
||||
func deviceManager(shouldConnectToDevice: Bool) {
|
||||
guard !isUpdatingFlag else {
|
||||
return
|
||||
}
|
||||
self.shouldConnect = shouldConnectToDevice
|
||||
}
|
||||
|
||||
func deviceManager(didReceive data: Data) {
|
||||
defer {
|
||||
@ -232,20 +245,21 @@ extension BluetoothClient: DeviceManagerDelegate {
|
||||
return
|
||||
case .invalidNumberOfBytesToDelete:
|
||||
guard case .clearRecordingBuffer = runningRequest else {
|
||||
// If clearing the recording buffer fails due to byte mismatch,
|
||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||
addRequest(.getInfo)
|
||||
log.error("Request \(runningRequest) received non-matching response about number of bytes to delete")
|
||||
return
|
||||
}
|
||||
log.error("Request \(runningRequest) received non-matching responde about number of bytes to delete")
|
||||
// If clearing the recording buffer fails due to byte mismatch,
|
||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||
addRequest(.getInfo)
|
||||
|
||||
case .responseTooLarge:
|
||||
guard case .getRecordingData = runningRequest else {
|
||||
// If requesting bytes fails due to the response size,
|
||||
// then requesting new info will update the response size, and the transfer will be resumed
|
||||
addRequest(.getInfo)
|
||||
log.error("Unexpectedly exceeded payload size for request \(runningRequest)")
|
||||
return
|
||||
}
|
||||
log.error("Unexpectedly exceeded payload size for request \(runningRequest)")
|
||||
// If requesting bytes fails due to the response size,
|
||||
// then requesting new info will update the response size, and the transfer will be resumed
|
||||
addRequest(.getInfo)
|
||||
default:
|
||||
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
||||
// If clearing the recording buffer fails due to byte mismatch,
|
||||
@ -264,10 +278,6 @@ extension BluetoothClient: DeviceManagerDelegate {
|
||||
didReceive(data: payload, offset: offset, count: count)
|
||||
case .clearRecordingBuffer:
|
||||
didClearDeviceStorage()
|
||||
|
||||
case .setDeviceStartTime:
|
||||
log.info("Device time set")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,11 +286,12 @@ extension BluetoothClient: DeviceManagerDelegate {
|
||||
log.warning("No running transfer after clearing device storage")
|
||||
return
|
||||
}
|
||||
runningTransfer.completeTransfer()
|
||||
defer { self.runningTransfer = nil }
|
||||
guard runningTransfer.completeTransfer() else {
|
||||
return
|
||||
}
|
||||
storage.add(runningTransfer.measurements)
|
||||
self.runningTransfer = nil
|
||||
|
||||
updateDeviceTimeIfNeeded()
|
||||
storage.lastDeviceTime = runningTransfer.time
|
||||
}
|
||||
|
||||
func deviceManager(didChangeState state: DeviceState) {
|
||||
@ -290,3 +301,4 @@ extension BluetoothClient: DeviceManagerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
/*
|
||||
enum BluetoothRequest {
|
||||
/**
|
||||
* Request the number of bytes already recorded
|
||||
@ -44,11 +44,6 @@ enum BluetoothRequest {
|
||||
*/
|
||||
case clearRecordingBuffer(byteCount: Int)
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
case setDeviceStartTime(deviceStartTimeSeconds: Int)
|
||||
|
||||
var serialized: Data {
|
||||
let firstByte = Data([byte])
|
||||
switch self {
|
||||
@ -58,8 +53,6 @@ enum BluetoothRequest {
|
||||
return firstByte + count.twoByteData + offset.twoByteData
|
||||
case .clearRecordingBuffer(let byteCount):
|
||||
return firstByte + byteCount.twoByteData
|
||||
case .setDeviceStartTime(let deviceStartTimeSeconds):
|
||||
return firstByte + deviceStartTimeSeconds.fourByteData
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +61,7 @@ enum BluetoothRequest {
|
||||
case .getInfo: return 0
|
||||
case .getRecordingData: return 1
|
||||
case .clearRecordingBuffer: return 2
|
||||
case .setDeviceStartTime: return 3
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -1,39 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
struct DeviceInfo {
|
||||
|
||||
let receivedDate: Date
|
||||
|
||||
|
||||
/**
|
||||
The maximum factor by which the device clock can run
|
||||
*/
|
||||
private let maximumTimeDilationFactor: Double = 0.01
|
||||
|
||||
|
||||
/// The number of bytes recorded by the tracker
|
||||
let numberOfRecordedBytes: Int
|
||||
|
||||
/// The number of measurements already performed
|
||||
let numberOfStoredMeasurements: Int
|
||||
|
||||
/// The measurements since device start
|
||||
let totalNumberOfMeasurements: Int
|
||||
|
||||
/// The interval between measurements (in seconds)
|
||||
let measurementInterval: Int
|
||||
|
||||
let nextMeasurement: Date
|
||||
|
||||
let sensor0: TemperatureSensor?
|
||||
|
||||
let sensor1: TemperatureSensor?
|
||||
|
||||
// MARK: Device time
|
||||
|
||||
/**
|
||||
The number of seconds the device has been powered on
|
||||
*/
|
||||
let numberOfSecondsRunning: Int
|
||||
|
||||
let deviceStartTime: Date
|
||||
|
||||
let hasDeviceStartTimeSet: Bool
|
||||
|
||||
let wakeupReason: DeviceWakeCause
|
||||
|
||||
let time: DeviceTime
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
@ -49,48 +41,77 @@ struct DeviceInfo {
|
||||
var storageFillPercentage: Int {
|
||||
Int((storageFillRatio * 100).rounded())
|
||||
}
|
||||
|
||||
var clockOffset: TimeInterval {
|
||||
// Measurements are performed on device start (-1) and also count next measurement (+1)
|
||||
let nextMeasurementTime = deviceStartTime.adding(seconds: totalNumberOfMeasurements * measurementInterval)
|
||||
return nextMeasurement.timeIntervalSince(nextMeasurementTime)
|
||||
|
||||
var currentMeasurementStartTime: Date {
|
||||
time.nextMeasurement.addingTimeInterval(-Double(numberOfStoredMeasurements * measurementInterval))
|
||||
}
|
||||
|
||||
var calculatedDeviceStartTime: Date {
|
||||
let runtime = totalNumberOfMeasurements * measurementInterval
|
||||
return nextMeasurement.adding(seconds: -runtime)
|
||||
func estimatedTimeDilation(to previous: DeviceTime?) -> (start: Date, dilation: Double) {
|
||||
let trivialResult = (start: currentMeasurementStartTime, dilation: 1.0)
|
||||
guard let previous else {
|
||||
log.info("No previous device time to compare")
|
||||
return trivialResult
|
||||
}
|
||||
// Check if device was restarted in between
|
||||
guard time.secondsSincePowerOn >= previous.secondsSincePowerOn else {
|
||||
log.info("Device restarted (runtime decreased from \(previous.secondsSincePowerOn) to \(time.secondsSincePowerOn))")
|
||||
return trivialResult
|
||||
}
|
||||
let newMeasurementCount = time.totalNumberOfMeasurements - previous.totalNumberOfMeasurements
|
||||
guard newMeasurementCount >= 0 else {
|
||||
log.info("Device restarted (measurements decreased from \(previous.totalNumberOfMeasurements) to \(time.totalNumberOfMeasurements))")
|
||||
return trivialResult
|
||||
}
|
||||
guard newMeasurementCount > 0 else {
|
||||
log.warning("No new measurements to calculate time difference")
|
||||
return trivialResult
|
||||
}
|
||||
|
||||
// Check that no measurements are missing
|
||||
|
||||
// Calculate the difference between the expected time for the next measurement and the device time
|
||||
let deviceTimeDifference = Double(newMeasurementCount * measurementInterval)
|
||||
let expectedNextMeasurement = previous.nextMeasurement.addingTimeInterval(deviceTimeDifference)
|
||||
let timeDifference = time.nextMeasurement.timeIntervalSince(expectedNextMeasurement)
|
||||
|
||||
let realTimeDifference = time.nextMeasurement.timeIntervalSince(previous.nextMeasurement)
|
||||
let timeDilation = realTimeDifference / deviceTimeDifference
|
||||
|
||||
log.info("Device time dilation \(timeDilation) (difference \(timeDifference))")
|
||||
|
||||
guard abs(timeDilation - 1.0) < maximumTimeDilationFactor else {
|
||||
log.warning("Device time too different from expected value (difference \(timeDifference) s)")
|
||||
return (currentMeasurementStartTime, 1.0)
|
||||
}
|
||||
return (previous.nextMeasurement, timeDilation)
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceInfo {
|
||||
|
||||
init(info: Data) throws {
|
||||
let date = Date()
|
||||
|
||||
var data = info
|
||||
|
||||
let date = Date().nearestSecond
|
||||
self.receivedDate = date
|
||||
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
||||
self.nextMeasurement = date.adding(seconds: try data.decodeTwoByteInteger())
|
||||
let secondsUntilNextMeasurement = try data.decodeTwoByteInteger()
|
||||
self.measurementInterval = try data.decodeTwoByteInteger()
|
||||
self.numberOfStoredMeasurements = try data.decodeTwoByteInteger()
|
||||
self.totalNumberOfMeasurements = try data.decodeFourByteInteger()
|
||||
let totalNumberOfMeasurements = try data.decodeFourByteInteger()
|
||||
self.transferBlockSize = try data.decodeTwoByteInteger()
|
||||
self.storageSize = try data.decodeTwoByteInteger()
|
||||
let secondsSincePowerOn = try data.decodeFourByteInteger()
|
||||
self.numberOfSecondsRunning = secondsSincePowerOn
|
||||
let deviceStartTimeSeconds = try data.decodeFourByteInteger()
|
||||
|
||||
self.time = .init(
|
||||
date: date,
|
||||
secondsSincePowerOn: secondsSincePowerOn,
|
||||
totalNumberOfMeasurements: totalNumberOfMeasurements,
|
||||
secondsUntilNextMeasurement: secondsUntilNextMeasurement)
|
||||
let _ = try data.decodeFourByteInteger()
|
||||
self.sensor0 = try data.decodeSensor()
|
||||
self.sensor1 = try data.decodeSensor()
|
||||
self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED
|
||||
|
||||
if deviceStartTimeSeconds != 0 {
|
||||
self.hasDeviceStartTimeSet = true
|
||||
self.deviceStartTime = Date(seconds: deviceStartTimeSeconds)
|
||||
|
||||
} else {
|
||||
self.hasDeviceStartTimeSet = false
|
||||
self.deviceStartTime = Date(seconds: date.seconds - secondsSincePowerOn) // Round to nearest second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,18 +119,13 @@ extension DeviceInfo {
|
||||
|
||||
static var mock: DeviceInfo {
|
||||
.init(
|
||||
receivedDate: Date(),
|
||||
numberOfRecordedBytes: 123,
|
||||
numberOfStoredMeasurements: 234,
|
||||
totalNumberOfMeasurements: 345,
|
||||
measurementInterval: 60,
|
||||
nextMeasurement: .now.addingTimeInterval(5),
|
||||
sensor0: .init(address: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], value: .value(21.0), date: .now.addingTimeInterval(-2)),
|
||||
sensor1: .init(address: [0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09], value: .value(19.0), date: .now.addingTimeInterval(-4)),
|
||||
numberOfSecondsRunning: 20,
|
||||
deviceStartTime: .now.addingTimeInterval(-20755),
|
||||
hasDeviceStartTimeSet: true,
|
||||
wakeupReason: .WAKEUP_EXT0,
|
||||
time: .mock,
|
||||
storageSize: 10000,
|
||||
transferBlockSize: 180)
|
||||
}
|
||||
|
@ -24,16 +24,13 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
||||
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
|
||||
private var dataUpdateTimer: Timer?
|
||||
|
||||
@discardableResult
|
||||
func connect() -> Bool {
|
||||
switch state {
|
||||
case .bluetoothDisabled:
|
||||
log.info("Can't connect, bluetooth disabled")
|
||||
return false
|
||||
case .disconnected, .bluetoothEnabled:
|
||||
case .disconnected:
|
||||
break
|
||||
default:
|
||||
return true
|
||||
@ -42,18 +39,53 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
||||
state = .scanning
|
||||
return true
|
||||
}
|
||||
shouldConnectIfPossible = true
|
||||
if !shouldConnectIfPossible {
|
||||
shouldConnectIfPossible = true
|
||||
}
|
||||
state = .scanning
|
||||
manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID])
|
||||
return true
|
||||
}
|
||||
|
||||
private var shouldConnectIfPossible = true
|
||||
var shouldConnectIfPossible = true {
|
||||
didSet {
|
||||
updateConnectionOnChange()
|
||||
delegate?.deviceManager(shouldConnectToDevice: shouldConnectIfPossible)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateConnectionOnChange() {
|
||||
if shouldConnectIfPossible {
|
||||
ensureConnection()
|
||||
} else {
|
||||
disconnectIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureConnection() {
|
||||
switch state {
|
||||
case .disconnected:
|
||||
connect()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func disconnectIfNeeded() {
|
||||
switch state {
|
||||
case .bluetoothDisabled, .disconnected:
|
||||
return
|
||||
default:
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
shouldConnectIfPossible = false
|
||||
if shouldConnectIfPossible {
|
||||
shouldConnectIfPossible = false
|
||||
}
|
||||
switch state {
|
||||
case .bluetoothDisabled, .bluetoothEnabled:
|
||||
case .bluetoothDisabled, .disconnected:
|
||||
return
|
||||
case .scanning:
|
||||
manager.stopScan()
|
||||
@ -67,8 +99,6 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
||||
manager.stopScan()
|
||||
state = .disconnected
|
||||
return
|
||||
case .disconnected:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +121,9 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||
guard shouldConnectIfPossible else {
|
||||
return
|
||||
}
|
||||
peripheral.delegate = self
|
||||
manager.connect(peripheral)
|
||||
manager.stopScan()
|
||||
@ -102,7 +135,7 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
||||
case .poweredOff:
|
||||
state = .bluetoothDisabled
|
||||
case .poweredOn:
|
||||
state = .bluetoothEnabled
|
||||
state = .disconnected
|
||||
connect()
|
||||
case .unsupported:
|
||||
state = .bluetoothDisabled
|
||||
|
@ -1,6 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
protocol DeviceManagerDelegate: AnyObject {
|
||||
|
||||
func deviceManager(shouldConnectToDevice: Bool)
|
||||
|
||||
func deviceManager(didReceive data: Data)
|
||||
|
||||
|
@ -4,8 +4,6 @@ import CoreBluetooth
|
||||
enum DeviceState {
|
||||
|
||||
case bluetoothDisabled
|
||||
|
||||
case bluetoothEnabled
|
||||
|
||||
case scanning
|
||||
|
||||
@ -23,8 +21,6 @@ enum DeviceState {
|
||||
switch self {
|
||||
case .bluetoothDisabled:
|
||||
return "Bluetooth is disabled"
|
||||
case .bluetoothEnabled:
|
||||
return "Bluetooth enabled"
|
||||
case .scanning:
|
||||
return "Scanning..."
|
||||
case .connecting(let device):
|
||||
@ -45,6 +41,18 @@ enum DeviceState {
|
||||
return "Not connected"
|
||||
}
|
||||
}
|
||||
|
||||
var device: CBPeripheral? {
|
||||
switch self {
|
||||
case .bluetoothDisabled, .disconnected, .scanning:
|
||||
return nil
|
||||
case .connecting(let device),
|
||||
.discoveringCharacteristic(let device),
|
||||
.discoveringServices(device: let device),
|
||||
.configured(let device, _):
|
||||
return device
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceState: CustomStringConvertible {
|
||||
@ -53,8 +61,6 @@ extension DeviceState: CustomStringConvertible {
|
||||
switch self {
|
||||
case .bluetoothDisabled:
|
||||
return "Bluetooth disabled"
|
||||
case .bluetoothEnabled:
|
||||
return "Bluetooth enabled"
|
||||
case .scanning:
|
||||
return "Searching for device"
|
||||
case .connecting:
|
||||
|
70
TempTrack/Bluetooth/DeviceTime.swift
Normal file
70
TempTrack/Bluetooth/DeviceTime.swift
Normal file
@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
|
||||
struct DeviceTime {
|
||||
|
||||
let date: Date
|
||||
|
||||
let secondsSincePowerOn: Int
|
||||
|
||||
let totalNumberOfMeasurements: Int
|
||||
|
||||
let secondsUntilNextMeasurement: Int
|
||||
|
||||
var nextMeasurement: Date {
|
||||
date.adding(seconds: secondsUntilNextMeasurement)
|
||||
}
|
||||
|
||||
var deviceStartTime: Date {
|
||||
date.adding(seconds: -secondsSincePowerOn)
|
||||
}
|
||||
|
||||
var estimatedMeasurementInterval: TimeInterval {
|
||||
guard totalNumberOfMeasurements > 0 else {
|
||||
return 60
|
||||
}
|
||||
return Double(secondsSincePowerOn + secondsUntilNextMeasurement) / Double(totalNumberOfMeasurements)
|
||||
}
|
||||
|
||||
func measurementStartTime(measurementInterval interval: TimeInterval) -> Date {
|
||||
nextMeasurement.addingTimeInterval(-Double(totalNumberOfMeasurements) * interval)
|
||||
}
|
||||
|
||||
func measurementOffset(measurementInterval interval: TimeInterval) -> TimeInterval {
|
||||
measurementStartTime(measurementInterval: interval).timeIntervalSince(deviceStartTime)
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceTime: Equatable {
|
||||
|
||||
}
|
||||
|
||||
extension DeviceTime: Codable {
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
let time = try container.decode(Double.self)
|
||||
self.date = .init(timeIntervalSince1970: time)
|
||||
self.secondsSincePowerOn = try container.decode(Int.self)
|
||||
self.totalNumberOfMeasurements = try container.decode(Int.self)
|
||||
self.secondsUntilNextMeasurement = try container.decode(Int.self)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
try container.encode(date.timeIntervalSince1970)
|
||||
try container.encode(secondsSincePowerOn)
|
||||
try container.encode(totalNumberOfMeasurements)
|
||||
try container.encode(secondsUntilNextMeasurement)
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceTime {
|
||||
|
||||
static var mock: DeviceTime {
|
||||
.init(
|
||||
date: .now,
|
||||
secondsSincePowerOn: 125,
|
||||
totalNumberOfMeasurements: 3,
|
||||
secondsUntilNextMeasurement: 55)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user