Add icon, set device time

This commit is contained in:
Christoph Hagen 2023-06-08 14:57:40 +02:00
parent 147cd6a306
commit 0f2246cbe5
15 changed files with 335 additions and 195 deletions

View File

@ -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 */,

View File

@ -1,6 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "app.png",
"idiom" : "universal", "idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@ -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
} }
} }

View File

@ -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)
}
}

View 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
}

View File

@ -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)
} }

View File

@ -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 {

View 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)
}
}

View File

@ -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)
}
} }

View 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)
}
}

View 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)
}
}

View File

@ -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

View File

@ -67,119 +67,132 @@ struct DeviceInfoView: View {
} }
func sensorView(_ sensor: TemperatureSensor?, id: Int) -> some View { func sensorView(_ sensor: TemperatureSensor?, id: Int) -> some View {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("Sensor \(id)") Text("Sensor \(id)")
.font(.headline) .font(.headline)
if let sensor { if let sensor {
HStack { HStack {
Image(systemSymbol: sensor.temperatureIcon) Image(systemSymbol: sensor.temperatureIcon)
.frame(width: 30) .frame(width: 30)
Text(sensor.temperatureText) Text(sensor.temperatureText)
} }
HStack { HStack {
Image(systemSymbol: .arrowTriangle2Circlepath) Image(systemSymbol: .arrowTriangle2Circlepath)
.frame(width: 30) .frame(width: 30)
Text(sensor.updateText) Text(sensor.updateText)
Spacer() Spacer()
} }
HStack { HStack {
Image(systemSymbol: .tag) Image(systemSymbol: .tag)
.frame(width: 30) .frame(width: 30)
Text(sensor.hexAddress) Text(sensor.hexAddress)
} }
} else { } else {
HStack { HStack {
Image(systemSymbol: .thermometerMediumSlash) Image(systemSymbol: .thermometerMediumSlash)
.frame(width: 30) .frame(width: 30)
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 {
VStack(alignment: .leading, spacing: 5) { NavigationView {
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") VStack(alignment: .leading, spacing: 5) {
.font(.headline) Text("Recording")
HStack { .font(.headline)
Image(systemSymbol: .power) HStack {
.frame(width: 30) Image(systemSymbol: .power)
Text(df.string(from: info.deviceStartTime)) .frame(width: 30)
Spacer() Text(df.string(from: info.deviceStartTime))
Spacer()
}
HStack {
Image(systemSymbol: .clock)
.frame(width: 30)
Text("\(runTimeString)")
}
HStack {
Image(systemSymbol: .clockBadgeExclamationmark)
.frame(width: 30)
Text(clockOffsetText)
}
HStack {
Image(systemSymbol: .stopwatch)
.frame(width: 30)
Text("Every \(info.measurementInterval) seconds")
Spacer()
}
HStack {
Image(systemSymbol: .arrowTriangle2Circlepath)
.frame(width: 30)
Text(nextUpdateText)
Spacer()
}
} }
HStack { VStack(alignment: .leading, spacing: 5) {
Image(systemSymbol: .clock) Text("Storage")
.frame(width: 30) .font(.headline)
Text("\(runTimeString)") HStack {
Image(systemSymbol: .speedometer)
.frame(width: 30)
Text("\(info.numberOfStoredMeasurements) Measurements (\(info.totalNumberOfMeasurements) total)")
}
HStack {
Image(systemSymbol: storageIcon)
.frame(width: 30)
Text(storageText)
}
HStack {
Image(systemSymbol: .iphoneAndArrowForward)
.frame(width: 30)
Text("\(info.transferBlockSize) Byte Block Size")
}
} }
HStack { sensorView(info.sensor0, id: 0)
Image(systemSymbol: .stopwatch) sensorView(info.sensor1, id: 1)
.frame(width: 30)
Text("Every \(info.measurementInterval) seconds")
Spacer()
}
HStack {
Image(systemSymbol: .arrowTriangle2Circlepath)
.frame(width: 30)
Text(nextUpdateText)
Spacer()
}
}
VStack(alignment: .leading, spacing: 5) {
Text("Storage")
.font(.headline)
HStack {
Image(systemSymbol: .speedometer)
.frame(width: 30)
Text("\(info.numberOfMeasurements) Measurements")
}
HStack {
Image(systemSymbol: storageIcon)
.frame(width: 30)
Text(storageText)
}
HStack {
Image(systemSymbol: .iphoneAndArrowForward)
.frame(width: 30)
Text("\(info.transferBlockSize) Byte Block Size")
}
}
sensorView(info.sensor0, id: 0)
sensorView(info.sensor1, id: 1)
Spacer()
HStack {
Spacer() Spacer()
TimelineView(.periodic(from: Date(), by: 1)) { context in HStack {
Text(updateText) Spacer()
.font(.footnote) TimelineView(.periodic(from: Date(), by: 1)) { context in
.textCase(.uppercase) Text(updateText)
.font(.footnote)
.textCase(.uppercase)
}
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))
} }
} }

BIN
icon.key Executable file

Binary file not shown.