diff --git a/TempTrack.xcodeproj/project.pbxproj b/TempTrack.xcodeproj/project.pbxproj index e1ca96b..d4cd673 100644 --- a/TempTrack.xcodeproj/project.pbxproj +++ b/TempTrack.xcodeproj/project.pbxproj @@ -14,6 +14,10 @@ 88404DDB2A2F4DCA00D30244 /* MeasurementDailyCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */; }; 88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88404DDC2A2F587400D30244 /* HistoryListRow.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 */; }; 88CDE0512A2508E900114294 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0502A2508E900114294 /* ContentView.swift */; }; 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 = ""; }; 88404DDC2A2F587400D30244 /* HistoryListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListRow.swift; sourceTree = ""; }; 88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDayOverview.swift; sourceTree = ""; }; + 88404DE02A31CA6B00D30244 /* BluetoothResponseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothResponseType.swift; sourceTree = ""; }; + 88404DE22A31F20E00D30244 /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extensions.swift"; sourceTree = ""; }; + 88404DE42A31F23E00D30244 /* UInt16+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt16+Extensions.swift"; sourceTree = ""; }; + 88404DE82A31F7D500D30244 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; 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 = ""; }; 88CDE0502A2508E900114294 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -119,7 +127,6 @@ 88CDE06E2A28AE8D00114294 /* Temperature */, 88CDE0522A2508EA00114294 /* Assets.xcassets */, 88CDE0542A2508EA00114294 /* Preview Content */, - 88CDE06C2A28A92000114294 /* DeviceInfo.swift */, ); path = TempTrack; sourceTree = ""; @@ -149,9 +156,11 @@ children = ( 88CDE0602A25108100114294 /* BluetoothClient.swift */, 88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */, + 88404DE02A31CA6B00D30244 /* BluetoothResponseType.swift */, 88CDE05C2A250F3C00114294 /* DeviceManager.swift */, 88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */, 88CDE05E2A250F5200114294 /* DeviceState.swift */, + 88CDE06C2A28A92000114294 /* DeviceInfo.swift */, ); path = Bluetooth; sourceTree = ""; @@ -174,6 +183,9 @@ E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */, 88404DD12A2F0D8F00D30244 /* Double+Extensions.swift */, 88404DD32A2F0DB100D30244 /* Date+Extensions.swift */, + 88404DE22A31F20E00D30244 /* Int+Extensions.swift */, + 88404DE42A31F23E00D30244 /* UInt16+Extensions.swift */, + 88404DE82A31F7D500D30244 /* Data+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -265,14 +277,18 @@ 88CDE05F2A250F5200114294 /* DeviceState.swift in Sources */, 88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */, 88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */, + 88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */, 88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */, 88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */, 88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */, 88CDE0722A28AEB900114294 /* TemperatureDataTransferDelegate.swift in Sources */, 88CDE0782A28AF2C00114294 /* TemperatureSensor.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 */, 88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */, + 88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */, 88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */, E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */, 88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */, diff --git a/TempTrack/Assets.xcassets/AppIcon.appiconset/Contents.json b/TempTrack/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..d20dba3 100644 --- a/TempTrack/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/TempTrack/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "app.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/TempTrack/Assets.xcassets/AppIcon.appiconset/app.png b/TempTrack/Assets.xcassets/AppIcon.appiconset/app.png new file mode 100644 index 0000000..eba285c Binary files /dev/null and b/TempTrack/Assets.xcassets/AppIcon.appiconset/app.png differ diff --git a/TempTrack/Bluetooth/BluetoothClient.swift b/TempTrack/Bluetooth/BluetoothClient.swift index c23867f..63958cc 100644 --- a/TempTrack/Bluetooth/BluetoothClient.swift +++ b/TempTrack/Bluetooth/BluetoothClient.swift @@ -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 } } diff --git a/TempTrack/Bluetooth/BluetoothRequest.swift b/TempTrack/Bluetooth/BluetoothRequest.swift index 9905ec1..bbc65b6 100644 --- a/TempTrack/Bluetooth/BluetoothRequest.swift +++ b/TempTrack/Bluetooth/BluetoothRequest.swift @@ -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) - } -} - diff --git a/TempTrack/Bluetooth/BluetoothResponseType.swift b/TempTrack/Bluetooth/BluetoothResponseType.swift new file mode 100644 index 0000000..a89f51b --- /dev/null +++ b/TempTrack/Bluetooth/BluetoothResponseType.swift @@ -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 +} + diff --git a/TempTrack/DeviceInfo.swift b/TempTrack/Bluetooth/DeviceInfo.swift similarity index 51% rename from TempTrack/DeviceInfo.swift rename to TempTrack/Bluetooth/DeviceInfo.swift index b9ebeea..a2514dd 100644 --- a/TempTrack/DeviceInfo.swift +++ b/TempTrack/Bluetooth/DeviceInfo.swift @@ -8,7 +8,10 @@ struct DeviceInfo { let numberOfRecordedBytes: Int /// 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) let measurementInterval: Int @@ -44,73 +47,60 @@ 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) + } } extension DeviceInfo { - static var size = 42 - - init?(info: Data) { - guard info.count == DeviceInfo.size else { - print("Invalid info size \(info.count)") - return nil - } - let data = Array(info) - self.receivedDate = Date() - self.numberOfRecordedBytes = .init(high: data[1], low: data[0]) - let secondsUntilNextMeasurement = UInt16(high: data[3], low: data[2]) - self.nextMeasurement = Date().addingTimeInterval(Double(secondsUntilNextMeasurement)) - 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]) + 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 - self.sensor0 = .init(address: Array(data[16..<24]), valueByte: data[14], secondsAgo: UInt16(high: data[33], low: data[32])) - self.sensor1 = .init(address: Array(data[24..<32]), valueByte: data[15], secondsAgo: UInt16(high: data[35], low: data[34])) - self.storageSize = .init(high: data[37], low: data[36]) - let deviceStartTimeSeconds = Int(uint32: data[41], data[40], data[39], data[38]) + 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 + 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 { static var mock: DeviceInfo { .init( receivedDate: Date(), numberOfRecordedBytes: 123, - numberOfMeasurements: 234, + 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(-1000), - hasDeviceStartTimeSet: false, + deviceStartTime: .now.addingTimeInterval(-20755), + hasDeviceStartTimeSet: true, storageSize: 10000, transferBlockSize: 180) } diff --git a/TempTrack/ContentView.swift b/TempTrack/ContentView.swift index 6ff05b8..8ae2455 100644 --- a/TempTrack/ContentView.swift +++ b/TempTrack/ContentView.swift @@ -126,7 +126,8 @@ struct ContentView: View { }.padding() } .padding() - .bottomSheet(isPresented: $showDeviceInfo, height: 600) { + .sheet(isPresented: $showDeviceInfo) { + //.bottomSheet(isPresented: $showDeviceInfo, height: 650) { if let info = bluetoothClient.deviceInfo { DeviceInfoView(info: info, isPresented: $showDeviceInfo) } else { diff --git a/TempTrack/Extensions/Data+Extensions.swift b/TempTrack/Extensions/Data+Extensions.swift new file mode 100644 index 0000000..691b212 --- /dev/null +++ b/TempTrack/Extensions/Data+Extensions.swift @@ -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.. Date { + addingTimeInterval(TimeInterval(seconds)) + } + + var nearestSecond: Date { + .init(seconds: seconds) + } } diff --git a/TempTrack/Extensions/Int+Extensions.swift b/TempTrack/Extensions/Int+Extensions.swift new file mode 100644 index 0000000..d8f794c --- /dev/null +++ b/TempTrack/Extensions/Int+Extensions.swift @@ -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) + } +} diff --git a/TempTrack/Extensions/UInt16+Extensions.swift b/TempTrack/Extensions/UInt16+Extensions.swift new file mode 100644 index 0000000..457b306 --- /dev/null +++ b/TempTrack/Extensions/UInt16+Extensions.swift @@ -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) + } +} diff --git a/TempTrack/Temperature/TemperatureDataTransfer.swift b/TempTrack/Temperature/TemperatureDataTransfer.swift index 491b7ad..0037bb8 100644 --- a/TempTrack/Temperature/TemperatureDataTransfer.swift +++ b/TempTrack/Temperature/TemperatureDataTransfer.swift @@ -34,7 +34,7 @@ final class TemperatureDataTransfer { init(info: DeviceInfo) { self.interval = info.measurementInterval - let recordingTime = info.numberOfMeasurements * info.measurementInterval + let recordingTime = info.numberOfStoredMeasurements * info.measurementInterval self.startDateOfCurrentTransfer = info.nextMeasurement.addingTimeInterval(-TimeInterval(recordingTime)) self.size = info.numberOfRecordedBytes self.blockSize = info.transferBlockSize diff --git a/TempTrack/Views/DeviceInfoView.swift b/TempTrack/Views/DeviceInfoView.swift index 4b852ec..9b4c8a9 100644 --- a/TempTrack/Views/DeviceInfoView.swift +++ b/TempTrack/Views/DeviceInfoView.swift @@ -67,119 +67,132 @@ struct DeviceInfoView: View { } func sensorView(_ sensor: TemperatureSensor?, id: Int) -> some View { - VStack(alignment: .leading, spacing: 5) { - Text("Sensor \(id)") - .font(.headline) - if let sensor { - HStack { - Image(systemSymbol: sensor.temperatureIcon) - .frame(width: 30) - Text(sensor.temperatureText) - } - HStack { - Image(systemSymbol: .arrowTriangle2Circlepath) - .frame(width: 30) - Text(sensor.updateText) - Spacer() - } - HStack { - Image(systemSymbol: .tag) - .frame(width: 30) - Text(sensor.hexAddress) - } - } else { - HStack { - Image(systemSymbol: .thermometerMediumSlash) - .frame(width: 30) - Text("Not connected") + VStack(alignment: .leading, spacing: 5) { + Text("Sensor \(id)") + .font(.headline) + if let sensor { + HStack { + Image(systemSymbol: sensor.temperatureIcon) + .frame(width: 30) + Text(sensor.temperatureText) + } + HStack { + Image(systemSymbol: .arrowTriangle2Circlepath) + .frame(width: 30) + Text(sensor.updateText) + Spacer() + } + HStack { + Image(systemSymbol: .tag) + .frame(width: 30) + Text(sensor.hexAddress) + } + } else { + HStack { + Image(systemSymbol: .thermometerMediumSlash) + .frame(width: 30) + Text("Not connected") + } } } - - } } var updateText: String { + guard info.receivedDate.secondsToNow > 3 else { + return "Updated Now" + } 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 { - 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) + NavigationView { VStack(alignment: .leading, spacing: 5) { - Text("Recording") - .font(.headline) - HStack { - Image(systemSymbol: .power) - .frame(width: 30) - Text(df.string(from: info.deviceStartTime)) - Spacer() + VStack(alignment: .leading, spacing: 5) { + Text("Recording") + .font(.headline) + HStack { + Image(systemSymbol: .power) + .frame(width: 30) + 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 { - Image(systemSymbol: .clock) - .frame(width: 30) - Text("\(runTimeString)") + VStack(alignment: .leading, spacing: 5) { + Text("Storage") + .font(.headline) + 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 { - Image(systemSymbol: .stopwatch) - .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 { + sensorView(info.sensor0, id: 0) + sensorView(info.sensor1, id: 1) Spacer() - TimelineView(.periodic(from: Date(), by: 1)) { context in - Text(updateText) - .font(.footnote) - .textCase(.uppercase) + HStack { + Spacer() + TimelineView(.periodic(from: Date(), by: 1)) { context in + Text(updateText) + .font(.footnote) + .textCase(.uppercase) + } + Spacer() } - Spacer() } - }.padding() + .padding() + .navigationTitle("Device Info") + .navigationBarTitleDisplayMode(.large) + } } } struct DeviceInfoView_Previews: PreviewProvider { static var previews: some View { DeviceInfoView(info: .mock, isPresented: .constant(true)) - .previewLayout(.fixed(width: 375, height: 600)) + .previewLayout(.fixed(width: 375, height: 650)) } } diff --git a/icon.key b/icon.key new file mode 100755 index 0000000..439ea77 Binary files /dev/null and b/icon.key differ