Add icon, set device time
This commit is contained in:
parent
147cd6a306
commit
0f2246cbe5
@ -14,6 +14,10 @@
|
|||||||
88404DDB2A2F4DCA00D30244 /* MeasurementDailyCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */; };
|
88404DDB2A2F4DCA00D30244 /* MeasurementDailyCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */; };
|
||||||
88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDC2A2F587400D30244 /* HistoryListRow.swift */; };
|
88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDC2A2F587400D30244 /* HistoryListRow.swift */; };
|
||||||
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */; };
|
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */; };
|
||||||
|
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DE02A31CA6B00D30244 /* BluetoothResponseType.swift */; };
|
||||||
|
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DE22A31F20E00D30244 /* Int+Extensions.swift */; };
|
||||||
|
88404DE52A31F23E00D30244 /* UInt16+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DE42A31F23E00D30244 /* UInt16+Extensions.swift */; };
|
||||||
|
88404DE92A31F7D500D30244 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DE82A31F7D500D30244 /* Data+Extensions.swift */; };
|
||||||
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE04E2A2508E900114294 /* TempTrackApp.swift */; };
|
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE04E2A2508E900114294 /* TempTrackApp.swift */; };
|
||||||
88CDE0512A2508E900114294 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0502A2508E900114294 /* ContentView.swift */; };
|
88CDE0512A2508E900114294 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0502A2508E900114294 /* ContentView.swift */; };
|
||||||
88CDE0532A2508EA00114294 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0522A2508EA00114294 /* Assets.xcassets */; };
|
88CDE0532A2508EA00114294 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0522A2508EA00114294 /* Assets.xcassets */; };
|
||||||
@ -45,6 +49,10 @@
|
|||||||
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementDailyCount.swift; sourceTree = "<group>"; };
|
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementDailyCount.swift; sourceTree = "<group>"; };
|
||||||
88404DDC2A2F587400D30244 /* HistoryListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListRow.swift; sourceTree = "<group>"; };
|
88404DDC2A2F587400D30244 /* HistoryListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListRow.swift; sourceTree = "<group>"; };
|
||||||
88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDayOverview.swift; sourceTree = "<group>"; };
|
88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDayOverview.swift; sourceTree = "<group>"; };
|
||||||
|
88404DE02A31CA6B00D30244 /* BluetoothResponseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothResponseType.swift; sourceTree = "<group>"; };
|
||||||
|
88404DE22A31F20E00D30244 /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
88404DE42A31F23E00D30244 /* UInt16+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt16+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
88404DE82A31F7D500D30244 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
88CDE04B2A2508E900114294 /* TempTrack.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TempTrack.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
88CDE04B2A2508E900114294 /* TempTrack.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TempTrack.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
88CDE04E2A2508E900114294 /* TempTrackApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTrackApp.swift; sourceTree = "<group>"; };
|
88CDE04E2A2508E900114294 /* TempTrackApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTrackApp.swift; sourceTree = "<group>"; };
|
||||||
88CDE0502A2508E900114294 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
88CDE0502A2508E900114294 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
@ -119,7 +127,6 @@
|
|||||||
88CDE06E2A28AE8D00114294 /* Temperature */,
|
88CDE06E2A28AE8D00114294 /* Temperature */,
|
||||||
88CDE0522A2508EA00114294 /* Assets.xcassets */,
|
88CDE0522A2508EA00114294 /* Assets.xcassets */,
|
||||||
88CDE0542A2508EA00114294 /* Preview Content */,
|
88CDE0542A2508EA00114294 /* Preview Content */,
|
||||||
88CDE06C2A28A92000114294 /* DeviceInfo.swift */,
|
|
||||||
);
|
);
|
||||||
path = TempTrack;
|
path = TempTrack;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -149,9 +156,11 @@
|
|||||||
children = (
|
children = (
|
||||||
88CDE0602A25108100114294 /* BluetoothClient.swift */,
|
88CDE0602A25108100114294 /* BluetoothClient.swift */,
|
||||||
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */,
|
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */,
|
||||||
|
88404DE02A31CA6B00D30244 /* BluetoothResponseType.swift */,
|
||||||
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
||||||
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
||||||
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
||||||
|
88CDE06C2A28A92000114294 /* DeviceInfo.swift */,
|
||||||
);
|
);
|
||||||
path = Bluetooth;
|
path = Bluetooth;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -174,6 +183,9 @@
|
|||||||
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */,
|
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */,
|
||||||
88404DD12A2F0D8F00D30244 /* Double+Extensions.swift */,
|
88404DD12A2F0D8F00D30244 /* Double+Extensions.swift */,
|
||||||
88404DD32A2F0DB100D30244 /* Date+Extensions.swift */,
|
88404DD32A2F0DB100D30244 /* Date+Extensions.swift */,
|
||||||
|
88404DE22A31F20E00D30244 /* Int+Extensions.swift */,
|
||||||
|
88404DE42A31F23E00D30244 /* UInt16+Extensions.swift */,
|
||||||
|
88404DE82A31F7D500D30244 /* Data+Extensions.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -265,14 +277,18 @@
|
|||||||
88CDE05F2A250F5200114294 /* DeviceState.swift in Sources */,
|
88CDE05F2A250F5200114294 /* DeviceState.swift in Sources */,
|
||||||
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */,
|
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */,
|
||||||
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */,
|
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */,
|
||||||
|
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */,
|
||||||
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */,
|
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */,
|
||||||
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
||||||
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */,
|
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */,
|
||||||
88CDE0722A28AEB900114294 /* TemperatureDataTransferDelegate.swift in Sources */,
|
88CDE0722A28AEB900114294 /* TemperatureDataTransferDelegate.swift in Sources */,
|
||||||
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */,
|
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */,
|
||||||
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */,
|
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */,
|
||||||
|
88404DE92A31F7D500D30244 /* Data+Extensions.swift in Sources */,
|
||||||
|
88404DE52A31F23E00D30244 /* UInt16+Extensions.swift in Sources */,
|
||||||
88404DD42A2F0DB100D30244 /* Date+Extensions.swift in Sources */,
|
88404DD42A2F0DB100D30244 /* Date+Extensions.swift in Sources */,
|
||||||
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
||||||
|
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */,
|
||||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
||||||
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */,
|
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */,
|
||||||
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "app.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
BIN
TempTrack/Assets.xcassets/AppIcon.appiconset/app.png
Normal file
BIN
TempTrack/Assets.xcassets/AppIcon.appiconset/app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 KiB |
@ -1,21 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
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 {
|
final class BluetoothClient: ObservableObject {
|
||||||
|
|
||||||
weak var delegate: TemperatureDataTransferDelegate?
|
weak var delegate: TemperatureDataTransferDelegate?
|
||||||
@ -24,8 +9,6 @@ final class BluetoothClient: ObservableObject {
|
|||||||
|
|
||||||
private let connection = DeviceManager()
|
private let connection = DeviceManager()
|
||||||
|
|
||||||
private var didTransferData = false
|
|
||||||
|
|
||||||
init(deviceInfo: DeviceInfo? = nil) {
|
init(deviceInfo: DeviceInfo? = nil) {
|
||||||
connection.delegate = self
|
connection.delegate = self
|
||||||
self.deviceInfo = deviceInfo
|
self.deviceInfo = deviceInfo
|
||||||
@ -43,7 +26,6 @@ final class BluetoothClient: ObservableObject {
|
|||||||
startRegularUpdates()
|
startRegularUpdates()
|
||||||
} else {
|
} else {
|
||||||
endRegularUpdates()
|
endRegularUpdates()
|
||||||
didTransferData = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,15 +33,8 @@ final class BluetoothClient: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
private(set) var deviceInfo: DeviceInfo? {
|
private(set) var deviceInfo: DeviceInfo? {
|
||||||
didSet {
|
didSet {
|
||||||
guard !didTransferData, runningTransfer == nil else {
|
updateDeviceTimeIfNeeded()
|
||||||
return
|
collectRecordedData()
|
||||||
}
|
|
||||||
guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard collectRecordedData() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +44,8 @@ final class BluetoothClient: ObservableObject {
|
|||||||
|
|
||||||
private var runningTransfer: TemperatureDataTransfer?
|
private var runningTransfer: TemperatureDataTransfer?
|
||||||
|
|
||||||
|
// MARK: Regular updates
|
||||||
|
|
||||||
func updateDeviceInfo() {
|
func updateDeviceInfo() {
|
||||||
guard case .configured = deviceState else {
|
guard case .configured = deviceState else {
|
||||||
return
|
return
|
||||||
@ -105,6 +82,8 @@ final class BluetoothClient: ObservableObject {
|
|||||||
print("Ending updates")
|
print("Ending updates")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Requests
|
||||||
|
|
||||||
private func performNextRequest() {
|
private func performNextRequest() {
|
||||||
guard runningRequest == nil else {
|
guard runningRequest == nil else {
|
||||||
return
|
return
|
||||||
@ -124,15 +103,45 @@ final class BluetoothClient: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addRequest(_ request: BluetoothRequest) {
|
func addRequest(_ request: BluetoothRequest) {
|
||||||
|
// TODO: Check if request already exists
|
||||||
openRequests.append(request)
|
openRequests.append(request)
|
||||||
performNextRequest()
|
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 {
|
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 {
|
guard let info = deviceInfo else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
guard info.numberOfStoredMeasurements > 0 else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
let transfer = TemperatureDataTransfer(info: info)
|
let transfer = TemperatureDataTransfer(info: info)
|
||||||
runningTransfer = transfer
|
runningTransfer = transfer
|
||||||
runningTransfer?.delegate = delegate
|
runningTransfer?.delegate = delegate
|
||||||
@ -154,17 +163,11 @@ final class BluetoothClient: ObservableObject {
|
|||||||
return // TODO: Start new transfer?
|
return // TODO: Start new transfer?
|
||||||
}
|
}
|
||||||
let next = runningTransfer.nextRequest()
|
let next = runningTransfer.nextRequest()
|
||||||
if case .clearRecordingBuffer = next {
|
|
||||||
runningTransfer.completeTransfer()
|
|
||||||
self.runningTransfer = nil
|
|
||||||
didTransferData = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addRequest(next)
|
addRequest(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func decode(info: Data) {
|
private func decode(info: Data) {
|
||||||
guard let newInfo = DeviceInfo(info: info) else {
|
guard let newInfo = try? DeviceInfo(info: info) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.deviceInfo = newInfo
|
self.deviceInfo = newInfo
|
||||||
@ -181,13 +184,13 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
|
|
||||||
func deviceManager(didReceive data: Data) {
|
func deviceManager(didReceive data: Data) {
|
||||||
defer {
|
defer {
|
||||||
self.runningRequest = nil
|
|
||||||
performNextRequest()
|
performNextRequest()
|
||||||
}
|
}
|
||||||
guard let runningRequest else {
|
guard let runningRequest else {
|
||||||
print("No request active, but \(data) received")
|
print("No request active, but \(data) received")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.runningRequest = nil
|
||||||
|
|
||||||
guard data.count > 0 else {
|
guard data.count > 0 else {
|
||||||
print("No response data for request \(runningRequest)")
|
print("No response data for request \(runningRequest)")
|
||||||
@ -198,14 +201,22 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
print("Unknown response \(data[0]) for request \(runningRequest)")
|
print("Unknown response \(data[0]) for request \(runningRequest)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard type == .success else {
|
switch type {
|
||||||
print("Error response \(data[0]) for request \(runningRequest)")
|
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,
|
// If clearing the recording buffer fails due to byte mismatch,
|
||||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||||
// If requesting bytes fails due to the response size,
|
// If requesting bytes fails due to the response size,
|
||||||
// then requesting new info will update the response size, and the transfer will be resumed
|
// then requesting new info will update the response size, and the transfer will be resumed
|
||||||
addRequest(.getInfo)
|
addRequest(.getInfo)
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
let payload = data.dropFirst()
|
let payload = data.dropFirst()
|
||||||
|
|
||||||
@ -217,6 +228,9 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
case .clearRecordingBuffer:
|
case .clearRecordingBuffer:
|
||||||
runningTransfer?.completeTransfer()
|
runningTransfer?.completeTransfer()
|
||||||
runningTransfer = nil
|
runningTransfer = nil
|
||||||
|
case .setDeviceStartTime:
|
||||||
|
print("Device time set")
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,11 @@ enum BluetoothRequest {
|
|||||||
*/
|
*/
|
||||||
case clearRecordingBuffer(byteCount: Int)
|
case clearRecordingBuffer(byteCount: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
*/
|
||||||
|
case setDeviceStartTime(deviceStartTimeSeconds: Int)
|
||||||
|
|
||||||
var serialized: Data {
|
var serialized: Data {
|
||||||
let firstByte = Data([byte])
|
let firstByte = Data([byte])
|
||||||
switch self {
|
switch self {
|
||||||
@ -53,6 +58,8 @@ enum BluetoothRequest {
|
|||||||
return firstByte + count.twoByteData + offset.twoByteData
|
return firstByte + count.twoByteData + offset.twoByteData
|
||||||
case .clearRecordingBuffer(let byteCount):
|
case .clearRecordingBuffer(let byteCount):
|
||||||
return firstByte + byteCount.twoByteData
|
return firstByte + byteCount.twoByteData
|
||||||
|
case .setDeviceStartTime(let deviceStartTimeSeconds):
|
||||||
|
return firstByte + deviceStartTimeSeconds.fourByteData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,26 +68,7 @@ enum BluetoothRequest {
|
|||||||
case .getInfo: return 0
|
case .getInfo: return 0
|
||||||
case .getRecordingData: return 1
|
case .getRecordingData: return 1
|
||||||
case .clearRecordingBuffer: return 2
|
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
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,10 @@ struct DeviceInfo {
|
|||||||
let numberOfRecordedBytes: Int
|
let numberOfRecordedBytes: Int
|
||||||
|
|
||||||
/// The number of measurements already performed
|
/// The number of measurements already performed
|
||||||
let numberOfMeasurements: Int
|
let numberOfStoredMeasurements: Int
|
||||||
|
|
||||||
|
/// The measurements since device start
|
||||||
|
let totalNumberOfMeasurements: Int
|
||||||
|
|
||||||
/// The interval between measurements (in seconds)
|
/// The interval between measurements (in seconds)
|
||||||
let measurementInterval: Int
|
let measurementInterval: Int
|
||||||
@ -44,73 +47,60 @@ struct DeviceInfo {
|
|||||||
var storageFillPercentage: Int {
|
var storageFillPercentage: Int {
|
||||||
Int((storageFillRatio * 100).rounded())
|
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 {
|
extension DeviceInfo {
|
||||||
|
|
||||||
static var size = 42
|
init(info: Data) throws {
|
||||||
|
var data = info
|
||||||
|
|
||||||
init?(info: Data) {
|
let date = Date().nearestSecond
|
||||||
guard info.count == DeviceInfo.size else {
|
self.receivedDate = date
|
||||||
print("Invalid info size \(info.count)")
|
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
||||||
return nil
|
self.nextMeasurement = date.adding(seconds: try data.decodeTwoByteInteger())
|
||||||
}
|
self.measurementInterval = try data.decodeTwoByteInteger()
|
||||||
let data = Array(info)
|
self.numberOfStoredMeasurements = try data.decodeTwoByteInteger()
|
||||||
self.receivedDate = Date()
|
self.totalNumberOfMeasurements = try data.decodeFourByteInteger()
|
||||||
self.numberOfRecordedBytes = .init(high: data[1], low: data[0])
|
self.transferBlockSize = try data.decodeTwoByteInteger()
|
||||||
let secondsUntilNextMeasurement = UInt16(high: data[3], low: data[2])
|
self.storageSize = try data.decodeTwoByteInteger()
|
||||||
self.nextMeasurement = Date().addingTimeInterval(Double(secondsUntilNextMeasurement))
|
let secondsSincePowerOn = try data.decodeFourByteInteger()
|
||||||
self.measurementInterval = .init(high: data[5], low: data[4])
|
|
||||||
self.numberOfMeasurements = .init(high: data[7], low: data[6])
|
|
||||||
self.transferBlockSize = .init(high: data[9], low: data[8])
|
|
||||||
let secondsSincePowerOn = Int(uint32: data[13], data[12], data[11], data[10])
|
|
||||||
self.numberOfSecondsRunning = secondsSincePowerOn
|
self.numberOfSecondsRunning = secondsSincePowerOn
|
||||||
self.sensor0 = .init(address: Array(data[16..<24]), valueByte: data[14], secondsAgo: UInt16(high: data[33], low: data[32]))
|
let deviceStartTimeSeconds = try data.decodeFourByteInteger()
|
||||||
self.sensor1 = .init(address: Array(data[24..<32]), valueByte: data[15], secondsAgo: UInt16(high: data[35], low: data[34]))
|
self.sensor0 = try data.decodeSensor()
|
||||||
self.storageSize = .init(high: data[37], low: data[36])
|
self.sensor1 = try data.decodeSensor()
|
||||||
let deviceStartTimeSeconds = Int(uint32: data[41], data[40], data[39], data[38])
|
|
||||||
if deviceStartTimeSeconds != 0 {
|
if deviceStartTimeSeconds != 0 {
|
||||||
self.hasDeviceStartTimeSet = true
|
self.hasDeviceStartTimeSet = true
|
||||||
self.deviceStartTime = Date(seconds: deviceStartTimeSeconds)
|
self.deviceStartTime = Date(seconds: deviceStartTimeSeconds)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
self.hasDeviceStartTimeSet = false
|
self.hasDeviceStartTimeSet = false
|
||||||
self.deviceStartTime = Date(seconds: Date().seconds - secondsSincePowerOn) // Round to nearest second
|
self.deviceStartTime = Date(seconds: date.seconds - secondsSincePowerOn) // Round to nearest second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UInt16 {
|
|
||||||
|
|
||||||
init(high: UInt8, low: UInt8) {
|
|
||||||
self = UInt16(high) << 8 + UInt16(low)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Int {
|
|
||||||
|
|
||||||
init(uint32 byte0: UInt8, _ byte1: UInt8, _ byte2: UInt8, _ byte3: UInt8) {
|
|
||||||
self = (Int(byte0) << 24) | (Int(byte1) << 16) | (Int(byte2) << 8) | Int(byte3)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(high: UInt8, low: UInt8) {
|
|
||||||
self = Int(high) << 8 + Int(low)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DeviceInfo {
|
extension DeviceInfo {
|
||||||
|
|
||||||
static var mock: DeviceInfo {
|
static var mock: DeviceInfo {
|
||||||
.init(
|
.init(
|
||||||
receivedDate: Date(),
|
receivedDate: Date(),
|
||||||
numberOfRecordedBytes: 123,
|
numberOfRecordedBytes: 123,
|
||||||
numberOfMeasurements: 234,
|
numberOfStoredMeasurements: 234,
|
||||||
|
totalNumberOfMeasurements: 345,
|
||||||
measurementInterval: 60,
|
measurementInterval: 60,
|
||||||
nextMeasurement: .now.addingTimeInterval(5),
|
nextMeasurement: .now.addingTimeInterval(5),
|
||||||
sensor0: .init(address: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], value: .value(21.0), date: .now.addingTimeInterval(-2)),
|
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)),
|
sensor1: .init(address: [0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09], value: .value(19.0), date: .now.addingTimeInterval(-4)),
|
||||||
numberOfSecondsRunning: 20,
|
numberOfSecondsRunning: 20,
|
||||||
deviceStartTime: .now.addingTimeInterval(-1000),
|
deviceStartTime: .now.addingTimeInterval(-20755),
|
||||||
hasDeviceStartTimeSet: false,
|
hasDeviceStartTimeSet: true,
|
||||||
storageSize: 10000,
|
storageSize: 10000,
|
||||||
transferBlockSize: 180)
|
transferBlockSize: 180)
|
||||||
}
|
}
|
@ -126,7 +126,8 @@ struct ContentView: View {
|
|||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.bottomSheet(isPresented: $showDeviceInfo, height: 600) {
|
.sheet(isPresented: $showDeviceInfo) {
|
||||||
|
//.bottomSheet(isPresented: $showDeviceInfo, height: 650) {
|
||||||
if let info = bluetoothClient.deviceInfo {
|
if let info = bluetoothClient.deviceInfo {
|
||||||
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
|
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
|
||||||
} else {
|
} else {
|
||||||
|
43
TempTrack/Extensions/Data+Extensions.swift
Normal file
43
TempTrack/Extensions/Data+Extensions.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum DeviceInfoError: Error {
|
||||||
|
case missingData
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
|
||||||
|
mutating func decodeUInt16() throws -> UInt16 {
|
||||||
|
guard count >= 2 else {
|
||||||
|
throw DeviceInfoError.missingData
|
||||||
|
}
|
||||||
|
let low = removeFirst()
|
||||||
|
let high = removeFirst()
|
||||||
|
return UInt16(high: high, low: low)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decodeTwoByteInteger() throws -> Int {
|
||||||
|
try decodeUInt16().integer
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decodeFourByteInteger() throws -> Int {
|
||||||
|
guard count >= 4 else {
|
||||||
|
throw DeviceInfoError.missingData
|
||||||
|
}
|
||||||
|
let byte0 = removeFirst()
|
||||||
|
let byte1 = removeFirst()
|
||||||
|
let byte2 = removeFirst()
|
||||||
|
let byte3 = removeFirst()
|
||||||
|
return (Int(byte3) << 24) | (Int(byte2) << 16) | (Int(byte1) << 8) | Int(byte0)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decodeSensor() throws -> TemperatureSensor? {
|
||||||
|
guard count >= 11 else {
|
||||||
|
throw DeviceInfoError.missingData
|
||||||
|
}
|
||||||
|
let address = Array(self[startIndex..<startIndex+8])
|
||||||
|
removeFirst(8)
|
||||||
|
let temperatureByte = removeFirst()
|
||||||
|
let time = try decodeUInt16()
|
||||||
|
return .init(address: address, valueByte: temperatureByte, secondsAgo: time)
|
||||||
|
}
|
||||||
|
}
|
@ -62,4 +62,12 @@ extension Date {
|
|||||||
var startOfNextDay: Date {
|
var startOfNextDay: Date {
|
||||||
startOfDay.addingTimeInterval(86400)
|
startOfDay.addingTimeInterval(86400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func adding(seconds: Int) -> Date {
|
||||||
|
addingTimeInterval(TimeInterval(seconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
var nearestSecond: Date {
|
||||||
|
.init(seconds: seconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
27
TempTrack/Extensions/Int+Extensions.swift
Normal file
27
TempTrack/Extensions/Int+Extensions.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Int {
|
||||||
|
|
||||||
|
var twoByteData: Data {
|
||||||
|
let value = UInt16(clamping: self)
|
||||||
|
return Data([value.low, value.high])
|
||||||
|
}
|
||||||
|
|
||||||
|
var fourByteData: Data {
|
||||||
|
let value = UInt32(clamping: self)
|
||||||
|
return Data([
|
||||||
|
UInt8(value & 0xFF),
|
||||||
|
UInt8((value >> 8) & 0xFF),
|
||||||
|
UInt8((value >> 16) & 0xFF),
|
||||||
|
UInt8((value >> 24) & 0xFF)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
init(uint32 byte0: UInt8, _ byte1: UInt8, _ byte2: UInt8, _ byte3: UInt8) {
|
||||||
|
self = (Int(byte0) << 24) | (Int(byte1) << 16) | (Int(byte2) << 8) | Int(byte3)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(high: UInt8, low: UInt8) {
|
||||||
|
self = Int(high) << 8 + Int(low)
|
||||||
|
}
|
||||||
|
}
|
20
TempTrack/Extensions/UInt16+Extensions.swift
Normal file
20
TempTrack/Extensions/UInt16+Extensions.swift
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension UInt16 {
|
||||||
|
|
||||||
|
init(high: UInt8, low: UInt8) {
|
||||||
|
self = UInt16(high) << 8 + UInt16(low)
|
||||||
|
}
|
||||||
|
|
||||||
|
var low: UInt8 {
|
||||||
|
UInt8(clamping: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var high: UInt8 {
|
||||||
|
UInt8(clamping: self >> 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
var integer: Int {
|
||||||
|
.init(self)
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ final class TemperatureDataTransfer {
|
|||||||
|
|
||||||
init(info: DeviceInfo) {
|
init(info: DeviceInfo) {
|
||||||
self.interval = info.measurementInterval
|
self.interval = info.measurementInterval
|
||||||
let recordingTime = info.numberOfMeasurements * info.measurementInterval
|
let recordingTime = info.numberOfStoredMeasurements * info.measurementInterval
|
||||||
self.startDateOfCurrentTransfer = info.nextMeasurement.addingTimeInterval(-TimeInterval(recordingTime))
|
self.startDateOfCurrentTransfer = info.nextMeasurement.addingTimeInterval(-TimeInterval(recordingTime))
|
||||||
self.size = info.numberOfRecordedBytes
|
self.size = info.numberOfRecordedBytes
|
||||||
self.blockSize = info.transferBlockSize
|
self.blockSize = info.transferBlockSize
|
||||||
|
@ -94,26 +94,30 @@ struct DeviceInfoView: View {
|
|||||||
Text("Not connected")
|
Text("Not connected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var updateText: String {
|
var updateText: String {
|
||||||
|
guard info.receivedDate.secondsToNow > 3 else {
|
||||||
|
return "Updated Now"
|
||||||
|
}
|
||||||
return "Updated \(info.receivedDate.timePassedText)"
|
return "Updated \(info.receivedDate.timePassedText)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clockOffsetText: String {
|
||||||
|
guard info.hasDeviceStartTimeSet else {
|
||||||
|
return "Clock not synchronized"
|
||||||
|
}
|
||||||
|
let offset = info.clockOffset.roundedInt
|
||||||
|
guard abs(offset) > 1 else {
|
||||||
|
return "No clock offset"
|
||||||
|
}
|
||||||
|
return "Offset: \(offset) seconds"
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
HStack {
|
|
||||||
Text("Device Info").font(.title2).bold()
|
|
||||||
Spacer()
|
|
||||||
Button(action: { isPresented = false }) {
|
|
||||||
Image(systemSymbol: .xmarkCircleFill)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.system(size: 26))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.bottom)
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("Recording")
|
Text("Recording")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@ -128,6 +132,11 @@ struct DeviceInfoView: View {
|
|||||||
.frame(width: 30)
|
.frame(width: 30)
|
||||||
Text("\(runTimeString)")
|
Text("\(runTimeString)")
|
||||||
}
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .clockBadgeExclamationmark)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(clockOffsetText)
|
||||||
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemSymbol: .stopwatch)
|
Image(systemSymbol: .stopwatch)
|
||||||
.frame(width: 30)
|
.frame(width: 30)
|
||||||
@ -147,7 +156,7 @@ struct DeviceInfoView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemSymbol: .speedometer)
|
Image(systemSymbol: .speedometer)
|
||||||
.frame(width: 30)
|
.frame(width: 30)
|
||||||
Text("\(info.numberOfMeasurements) Measurements")
|
Text("\(info.numberOfStoredMeasurements) Measurements (\(info.totalNumberOfMeasurements) total)")
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemSymbol: storageIcon)
|
Image(systemSymbol: storageIcon)
|
||||||
@ -172,14 +181,18 @@ struct DeviceInfoView: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}.padding()
|
}
|
||||||
|
.padding()
|
||||||
|
.navigationTitle("Device Info")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeviceInfoView_Previews: PreviewProvider {
|
struct DeviceInfoView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
DeviceInfoView(info: .mock, isPresented: .constant(true))
|
DeviceInfoView(info: .mock, isPresented: .constant(true))
|
||||||
.previewLayout(.fixed(width: 375, height: 600))
|
.previewLayout(.fixed(width: 375, height: 650))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user