Add icon, set device time
This commit is contained in:
@ -1,21 +1,6 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum BluetoothResponseType: UInt8 {
|
||||
|
||||
/// The response to the last request is provided
|
||||
case success = 0
|
||||
|
||||
/// Invalid command received
|
||||
case invalidCommand = 1
|
||||
|
||||
case responseTooLarge = 2
|
||||
|
||||
case unknownCommand = 3
|
||||
|
||||
case invalidNumberOfBytesToDelete = 4
|
||||
}
|
||||
|
||||
final class BluetoothClient: ObservableObject {
|
||||
|
||||
weak var delegate: TemperatureDataTransferDelegate?
|
||||
@ -24,8 +9,6 @@ final class BluetoothClient: ObservableObject {
|
||||
|
||||
private let connection = DeviceManager()
|
||||
|
||||
private var didTransferData = false
|
||||
|
||||
init(deviceInfo: DeviceInfo? = nil) {
|
||||
connection.delegate = self
|
||||
self.deviceInfo = deviceInfo
|
||||
@ -43,7 +26,6 @@ final class BluetoothClient: ObservableObject {
|
||||
startRegularUpdates()
|
||||
} else {
|
||||
endRegularUpdates()
|
||||
didTransferData = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -51,15 +33,8 @@ final class BluetoothClient: ObservableObject {
|
||||
@Published
|
||||
private(set) var deviceInfo: DeviceInfo? {
|
||||
didSet {
|
||||
guard !didTransferData, runningTransfer == nil else {
|
||||
return
|
||||
}
|
||||
guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else {
|
||||
return
|
||||
}
|
||||
guard collectRecordedData() else {
|
||||
return
|
||||
}
|
||||
updateDeviceTimeIfNeeded()
|
||||
collectRecordedData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,6 +44,8 @@ final class BluetoothClient: ObservableObject {
|
||||
|
||||
private var runningTransfer: TemperatureDataTransfer?
|
||||
|
||||
// MARK: Regular updates
|
||||
|
||||
func updateDeviceInfo() {
|
||||
guard case .configured = deviceState else {
|
||||
return
|
||||
@ -105,6 +82,8 @@ final class BluetoothClient: ObservableObject {
|
||||
print("Ending updates")
|
||||
}
|
||||
|
||||
// MARK: Requests
|
||||
|
||||
private func performNextRequest() {
|
||||
guard runningRequest == nil else {
|
||||
return
|
||||
@ -124,15 +103,45 @@ final class BluetoothClient: ObservableObject {
|
||||
}
|
||||
|
||||
func addRequest(_ request: BluetoothRequest) {
|
||||
// TODO: Check if request already exists
|
||||
openRequests.append(request)
|
||||
performNextRequest()
|
||||
}
|
||||
|
||||
// MARK: Device time
|
||||
|
||||
private func updateDeviceTimeIfNeeded() {
|
||||
guard let info = deviceInfo else {
|
||||
return
|
||||
}
|
||||
guard !info.hasDeviceStartTimeSet else {
|
||||
return
|
||||
}
|
||||
guard !openRequests.contains(where: { if case .setDeviceStartTime = $0 { return true }; return false }) else {
|
||||
return
|
||||
}
|
||||
let time = info.deviceStartTime.seconds
|
||||
addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time))
|
||||
print("Setting device start time to \(time) s (\(Date().seconds) current)")
|
||||
}
|
||||
|
||||
// MARK: Data transfer
|
||||
|
||||
@discardableResult
|
||||
func collectRecordedData() -> Bool {
|
||||
guard runningTransfer == nil else {
|
||||
return false
|
||||
}
|
||||
guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else {
|
||||
return false
|
||||
}
|
||||
guard let info = deviceInfo else {
|
||||
return false
|
||||
}
|
||||
guard info.numberOfStoredMeasurements > 0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
let transfer = TemperatureDataTransfer(info: info)
|
||||
runningTransfer = transfer
|
||||
runningTransfer?.delegate = delegate
|
||||
@ -154,17 +163,11 @@ final class BluetoothClient: ObservableObject {
|
||||
return // TODO: Start new transfer?
|
||||
}
|
||||
let next = runningTransfer.nextRequest()
|
||||
if case .clearRecordingBuffer = next {
|
||||
runningTransfer.completeTransfer()
|
||||
self.runningTransfer = nil
|
||||
didTransferData = true
|
||||
return
|
||||
}
|
||||
addRequest(next)
|
||||
}
|
||||
|
||||
private func decode(info: Data) {
|
||||
guard let newInfo = DeviceInfo(info: info) else {
|
||||
guard let newInfo = try? DeviceInfo(info: info) else {
|
||||
return
|
||||
}
|
||||
self.deviceInfo = newInfo
|
||||
@ -181,13 +184,13 @@ extension BluetoothClient: DeviceManagerDelegate {
|
||||
|
||||
func deviceManager(didReceive data: Data) {
|
||||
defer {
|
||||
self.runningRequest = nil
|
||||
performNextRequest()
|
||||
}
|
||||
guard let runningRequest else {
|
||||
print("No request active, but \(data) received")
|
||||
return
|
||||
}
|
||||
self.runningRequest = nil
|
||||
|
||||
guard data.count > 0 else {
|
||||
print("No response data for request \(runningRequest)")
|
||||
@ -198,14 +201,22 @@ extension BluetoothClient: DeviceManagerDelegate {
|
||||
print("Unknown response \(data[0]) for request \(runningRequest)")
|
||||
return
|
||||
}
|
||||
guard type == .success else {
|
||||
print("Error response \(data[0]) for request \(runningRequest)")
|
||||
switch type {
|
||||
case .success:
|
||||
break
|
||||
case .responseInProgress:
|
||||
// Retry the request
|
||||
addRequest(runningRequest)
|
||||
return
|
||||
default:
|
||||
print("Unknown response \(data[0]) for request \(runningRequest)")
|
||||
// If clearing the recording buffer fails due to byte mismatch,
|
||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||
// 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)
|
||||
return
|
||||
|
||||
}
|
||||
let payload = data.dropFirst()
|
||||
|
||||
@ -217,6 +228,9 @@ extension BluetoothClient: DeviceManagerDelegate {
|
||||
case .clearRecordingBuffer:
|
||||
runningTransfer?.completeTransfer()
|
||||
runningTransfer = nil
|
||||
case .setDeviceStartTime:
|
||||
print("Device time set")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,11 @@ enum BluetoothRequest {
|
||||
*/
|
||||
case clearRecordingBuffer(byteCount: Int)
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
case setDeviceStartTime(deviceStartTimeSeconds: Int)
|
||||
|
||||
var serialized: Data {
|
||||
let firstByte = Data([byte])
|
||||
switch self {
|
||||
@ -53,6 +58,8 @@ enum BluetoothRequest {
|
||||
return firstByte + count.twoByteData + offset.twoByteData
|
||||
case .clearRecordingBuffer(let byteCount):
|
||||
return firstByte + byteCount.twoByteData
|
||||
case .setDeviceStartTime(let deviceStartTimeSeconds):
|
||||
return firstByte + deviceStartTimeSeconds.fourByteData
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,26 +68,7 @@ enum BluetoothRequest {
|
||||
case .getInfo: return 0
|
||||
case .getRecordingData: return 1
|
||||
case .clearRecordingBuffer: return 2
|
||||
case .setDeviceStartTime: return 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Int {
|
||||
|
||||
var twoByteData: Data {
|
||||
let value = UInt16(clamping: self)
|
||||
return Data([value.low, value.high])
|
||||
}
|
||||
}
|
||||
|
||||
private extension UInt16 {
|
||||
|
||||
var low: UInt8 {
|
||||
UInt8(self & 0xFF)
|
||||
}
|
||||
|
||||
var high: UInt8 {
|
||||
UInt8((self >> 8) & 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
|
19
TempTrack/Bluetooth/BluetoothResponseType.swift
Normal file
19
TempTrack/Bluetooth/BluetoothResponseType.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
|
||||
enum BluetoothResponseType: UInt8 {
|
||||
|
||||
/// The response to the last request is provided
|
||||
case success = 0
|
||||
|
||||
/// Invalid command received
|
||||
case invalidCommand = 1
|
||||
|
||||
case responseTooLarge = 2
|
||||
|
||||
case unknownCommand = 3
|
||||
|
||||
case invalidNumberOfBytesToDelete = 4
|
||||
|
||||
case responseInProgress = 5
|
||||
}
|
||||
|
107
TempTrack/Bluetooth/DeviceInfo.swift
Normal file
107
TempTrack/Bluetooth/DeviceInfo.swift
Normal file
@ -0,0 +1,107 @@
|
||||
import Foundation
|
||||
|
||||
struct DeviceInfo {
|
||||
|
||||
let receivedDate: Date
|
||||
|
||||
/// 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
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
let storageSize: Int
|
||||
|
||||
/// The maximum number of bytes which can be requested
|
||||
let transferBlockSize: Int
|
||||
|
||||
var storageFillRatio: Double {
|
||||
Double(numberOfRecordedBytes) / Double(storageSize)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceInfo {
|
||||
|
||||
init(info: Data) throws {
|
||||
var data = info
|
||||
|
||||
let date = Date().nearestSecond
|
||||
self.receivedDate = date
|
||||
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
||||
self.nextMeasurement = date.adding(seconds: try data.decodeTwoByteInteger())
|
||||
self.measurementInterval = try data.decodeTwoByteInteger()
|
||||
self.numberOfStoredMeasurements = try data.decodeTwoByteInteger()
|
||||
self.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.sensor0 = try data.decodeSensor()
|
||||
self.sensor1 = try data.decodeSensor()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
storageSize: 10000,
|
||||
transferBlockSize: 180)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user