Fix transfer errors, save raw data
This commit is contained in:
parent
396571fd30
commit
d1a24b581d
@ -23,18 +23,13 @@
|
|||||||
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 */; };
|
||||||
88CDE0562A2508EA00114294 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0552A2508EA00114294 /* Preview Assets.xcassets */; };
|
88CDE0562A2508EA00114294 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0552A2508EA00114294 /* Preview Assets.xcassets */; };
|
||||||
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE05C2A250F3C00114294 /* DeviceManager.swift */; };
|
|
||||||
88CDE05F2A250F5200114294 /* DeviceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE05E2A250F5200114294 /* DeviceState.swift */; };
|
|
||||||
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0602A25108100114294 /* BluetoothClient.swift */; };
|
|
||||||
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */; };
|
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */; };
|
||||||
88CDE0662A25D08F00114294 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 88CDE0652A25D08F00114294 /* SFSafeSymbols */; };
|
88CDE0662A25D08F00114294 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 88CDE0652A25D08F00114294 /* SFSafeSymbols */; };
|
||||||
88CDE0682A2698B400114294 /* PersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0672A2698B400114294 /* PersistentStorage.swift */; };
|
88CDE0682A2698B400114294 /* PersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0672A2698B400114294 /* PersistentStorage.swift */; };
|
||||||
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE06C2A28A92000114294 /* DeviceInfo.swift */; };
|
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE06C2A28A92000114294 /* DeviceInfo.swift */; };
|
||||||
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE06F2A28AEA300114294 /* TemperatureMeasurement.swift */; };
|
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE06F2A28AEA300114294 /* TemperatureMeasurement.swift */; };
|
||||||
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */; };
|
|
||||||
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0752A28AF0900114294 /* TemperatureValue.swift */; };
|
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0752A28AF0900114294 /* TemperatureValue.swift */; };
|
||||||
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */; };
|
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */; };
|
||||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */; };
|
|
||||||
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; };
|
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; };
|
||||||
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */; };
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */; };
|
||||||
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */; };
|
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */; };
|
||||||
@ -45,7 +40,6 @@
|
|||||||
E2A554012A3A6403005204C3 /* DeviceTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554002A3A6403005204C3 /* DeviceTime.swift */; };
|
E2A554012A3A6403005204C3 /* DeviceTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554002A3A6403005204C3 /* DeviceTime.swift */; };
|
||||||
E2A554052A4ADA93005204C3 /* TransferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554042A4ADA93005204C3 /* TransferView.swift */; };
|
E2A554052A4ADA93005204C3 /* TransferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554042A4ADA93005204C3 /* TransferView.swift */; };
|
||||||
E2A554072A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554062A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift */; };
|
E2A554072A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554062A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift */; };
|
||||||
E2A554092A4ADCC9005204C3 /* DeviceConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554082A4ADCC9005204C3 /* DeviceConnection.swift */; };
|
|
||||||
E2A5540C2A4ADFC6005204C3 /* DeviceInfoRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540B2A4ADFC6005204C3 /* DeviceInfoRequest.swift */; };
|
E2A5540C2A4ADFC6005204C3 /* DeviceInfoRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540B2A4ADFC6005204C3 /* DeviceInfoRequest.swift */; };
|
||||||
E2A5540E2A4C9C4C005204C3 /* BluetoothRequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */; };
|
E2A5540E2A4C9C4C005204C3 /* BluetoothRequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */; };
|
||||||
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */; };
|
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */; };
|
||||||
@ -54,6 +48,7 @@
|
|||||||
E2E69B602A4CD48F00C6035E /* IconAndTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */; };
|
E2E69B602A4CD48F00C6035E /* IconAndTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */; };
|
||||||
E2E69B622A4D7C3100C6035E /* BluetoothScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */; };
|
E2E69B622A4D7C3100C6035E /* BluetoothScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */; };
|
||||||
E2E69B662A4DA48B00C6035E /* BluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */; };
|
E2E69B662A4DA48B00C6035E /* BluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */; };
|
||||||
|
E2E69B682A529FA800C6035E /* TransferHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B672A529FA800C6035E /* TransferHandler.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -73,17 +68,12 @@
|
|||||||
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>"; };
|
||||||
88CDE0522A2508EA00114294 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
88CDE0522A2508EA00114294 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
88CDE0552A2508EA00114294 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
88CDE0552A2508EA00114294 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
88CDE05C2A250F3C00114294 /* DeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceManager.swift; sourceTree = "<group>"; };
|
|
||||||
88CDE05E2A250F5200114294 /* DeviceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceState.swift; sourceTree = "<group>"; };
|
|
||||||
88CDE0602A25108100114294 /* BluetoothClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothClient.swift; sourceTree = "<group>"; };
|
|
||||||
88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDataTransfer.swift; sourceTree = "<group>"; };
|
88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDataTransfer.swift; sourceTree = "<group>"; };
|
||||||
88CDE0672A2698B400114294 /* PersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentStorage.swift; sourceTree = "<group>"; };
|
88CDE0672A2698B400114294 /* PersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentStorage.swift; sourceTree = "<group>"; };
|
||||||
88CDE06C2A28A92000114294 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
|
88CDE06C2A28A92000114294 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
|
||||||
88CDE06F2A28AEA300114294 /* TemperatureMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureMeasurement.swift; sourceTree = "<group>"; };
|
88CDE06F2A28AEA300114294 /* TemperatureMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureMeasurement.swift; sourceTree = "<group>"; };
|
||||||
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagerDelegate.swift; sourceTree = "<group>"; };
|
|
||||||
88CDE0752A28AF0900114294 /* TemperatureValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureValue.swift; sourceTree = "<group>"; };
|
88CDE0752A28AF0900114294 /* TemperatureValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureValue.swift; sourceTree = "<group>"; };
|
||||||
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureSensor.swift; sourceTree = "<group>"; };
|
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureSensor.swift; sourceTree = "<group>"; };
|
||||||
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRequest.swift; sourceTree = "<group>"; };
|
|
||||||
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = "<group>"; };
|
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = "<group>"; };
|
||||||
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
|
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureHistoryChart.swift; sourceTree = "<group>"; };
|
E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureHistoryChart.swift; sourceTree = "<group>"; };
|
||||||
@ -94,7 +84,6 @@
|
|||||||
E2A554002A3A6403005204C3 /* DeviceTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTime.swift; sourceTree = "<group>"; };
|
E2A554002A3A6403005204C3 /* DeviceTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTime.swift; sourceTree = "<group>"; };
|
||||||
E2A554042A4ADA93005204C3 /* TransferView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferView.swift; sourceTree = "<group>"; };
|
E2A554042A4ADA93005204C3 /* TransferView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferView.swift; sourceTree = "<group>"; };
|
||||||
E2A554062A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDataTransferDelegate.swift; sourceTree = "<group>"; };
|
E2A554062A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDataTransferDelegate.swift; sourceTree = "<group>"; };
|
||||||
E2A554082A4ADCC9005204C3 /* DeviceConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceConnection.swift; sourceTree = "<group>"; };
|
|
||||||
E2A5540B2A4ADFC6005204C3 /* DeviceInfoRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoRequest.swift; sourceTree = "<group>"; };
|
E2A5540B2A4ADFC6005204C3 /* DeviceInfoRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoRequest.swift; sourceTree = "<group>"; };
|
||||||
E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRequestType.swift; sourceTree = "<group>"; };
|
E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRequestType.swift; sourceTree = "<group>"; };
|
||||||
E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRequest.swift; sourceTree = "<group>"; };
|
E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRequest.swift; sourceTree = "<group>"; };
|
||||||
@ -103,6 +92,7 @@
|
|||||||
E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconAndTextView.swift; sourceTree = "<group>"; };
|
E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconAndTextView.swift; sourceTree = "<group>"; };
|
||||||
E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothScanner.swift; sourceTree = "<group>"; };
|
E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothScanner.swift; sourceTree = "<group>"; };
|
||||||
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevice.swift; sourceTree = "<group>"; };
|
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevice.swift; sourceTree = "<group>"; };
|
||||||
|
E2E69B672A529FA800C6035E /* TransferHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferHandler.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -185,12 +175,7 @@
|
|||||||
88CDE0792A28AF3E00114294 /* Bluetooth */ = {
|
88CDE0792A28AF3E00114294 /* Bluetooth */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
88CDE0602A25108100114294 /* BluetoothClient.swift */,
|
|
||||||
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */,
|
|
||||||
88404DE02A31CA6B00D30244 /* BluetoothResponseType.swift */,
|
88404DE02A31CA6B00D30244 /* BluetoothResponseType.swift */,
|
||||||
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
|
||||||
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
|
||||||
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
|
||||||
88CDE06C2A28A92000114294 /* DeviceInfo.swift */,
|
88CDE06C2A28A92000114294 /* DeviceInfo.swift */,
|
||||||
E2A554002A3A6403005204C3 /* DeviceTime.swift */,
|
E2A554002A3A6403005204C3 /* DeviceTime.swift */,
|
||||||
88404DEA2A37BE3000D30244 /* DeviceWakeCause.swift */,
|
88404DEA2A37BE3000D30244 /* DeviceWakeCause.swift */,
|
||||||
@ -230,7 +215,6 @@
|
|||||||
E2A5540A2A4ADD1D005204C3 /* Connection */ = {
|
E2A5540A2A4ADD1D005204C3 /* Connection */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E2A554082A4ADCC9005204C3 /* DeviceConnection.swift */,
|
|
||||||
E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */,
|
E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */,
|
||||||
E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */,
|
E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */,
|
||||||
E2A5540B2A4ADFC6005204C3 /* DeviceInfoRequest.swift */,
|
E2A5540B2A4ADFC6005204C3 /* DeviceInfoRequest.swift */,
|
||||||
@ -238,6 +222,7 @@
|
|||||||
E2A554152A4C9D2E005204C3 /* DeviceDataResetRequest.swift */,
|
E2A554152A4C9D2E005204C3 /* DeviceDataResetRequest.swift */,
|
||||||
E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */,
|
E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */,
|
||||||
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */,
|
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */,
|
||||||
|
E2E69B672A529FA800C6035E /* TransferHandler.swift */,
|
||||||
);
|
);
|
||||||
path = Connection;
|
path = Connection;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -323,13 +308,10 @@
|
|||||||
88404DDB2A2F4DCA00D30244 /* MeasurementDailyCount.swift in Sources */,
|
88404DDB2A2F4DCA00D30244 /* MeasurementDailyCount.swift in Sources */,
|
||||||
E2A554142A4C9C96005204C3 /* DeviceDataRequest.swift in Sources */,
|
E2A554142A4C9C96005204C3 /* DeviceDataRequest.swift in Sources */,
|
||||||
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
||||||
88CDE05F2A250F5200114294 /* DeviceState.swift in Sources */,
|
|
||||||
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */,
|
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */,
|
||||||
E2A554092A4ADCC9005204C3 /* DeviceConnection.swift in Sources */,
|
|
||||||
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */,
|
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */,
|
||||||
E2A554162A4C9D2E005204C3 /* DeviceDataResetRequest.swift in Sources */,
|
E2A554162A4C9D2E005204C3 /* DeviceDataResetRequest.swift in Sources */,
|
||||||
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */,
|
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */,
|
||||||
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */,
|
|
||||||
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
||||||
E2E69B602A4CD48F00C6035E /* IconAndTextView.swift in Sources */,
|
E2E69B602A4CD48F00C6035E /* IconAndTextView.swift in Sources */,
|
||||||
E2A554052A4ADA93005204C3 /* TransferView.swift in Sources */,
|
E2A554052A4ADA93005204C3 /* TransferView.swift in Sources */,
|
||||||
@ -344,7 +326,6 @@
|
|||||||
E2E69B622A4D7C3100C6035E /* BluetoothScanner.swift in Sources */,
|
E2E69B622A4D7C3100C6035E /* BluetoothScanner.swift in Sources */,
|
||||||
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */,
|
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */,
|
||||||
E2A5540C2A4ADFC6005204C3 /* DeviceInfoRequest.swift in Sources */,
|
E2A5540C2A4ADFC6005204C3 /* DeviceInfoRequest.swift in Sources */,
|
||||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
|
||||||
E2E69B662A4DA48B00C6035E /* BluetoothDevice.swift in Sources */,
|
E2E69B662A4DA48B00C6035E /* BluetoothDevice.swift in Sources */,
|
||||||
E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */,
|
E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */,
|
||||||
E2A553F92A399F58005204C3 /* Log.swift in Sources */,
|
E2A553F92A399F58005204C3 /* Log.swift in Sources */,
|
||||||
@ -353,9 +334,8 @@
|
|||||||
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
||||||
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */,
|
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */,
|
||||||
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
||||||
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */,
|
|
||||||
E2A554072A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift in Sources */,
|
E2A554072A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift in Sources */,
|
||||||
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */,
|
E2E69B682A529FA800C6035E /* TransferHandler.swift in Sources */,
|
||||||
88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */,
|
88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */,
|
||||||
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
||||||
E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */,
|
E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */,
|
||||||
|
Binary file not shown.
@ -1,304 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
/*
|
|
||||||
final class BluetoothClient: ObservableObject {
|
|
||||||
|
|
||||||
private let updateInterval = 3.0
|
|
||||||
|
|
||||||
private let minimumOffsetToUpdateDeviceClock = 5.0
|
|
||||||
|
|
||||||
private let connection = DeviceManager()
|
|
||||||
|
|
||||||
private let storage: PersistentStorage
|
|
||||||
|
|
||||||
var hasInfo: Bool {
|
|
||||||
deviceInfo != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var isConnected: Bool {
|
|
||||||
if case .configured = deviceState {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isUpdatingFlag = false
|
|
||||||
|
|
||||||
@Published
|
|
||||||
var shouldConnect: Bool {
|
|
||||||
didSet {
|
|
||||||
isUpdatingFlag = true
|
|
||||||
connection.shouldConnectIfPossible = shouldConnect
|
|
||||||
log.info("Should connect: \(shouldConnect)")
|
|
||||||
isUpdatingFlag = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(storage: PersistentStorage, shouldConnect: Bool = false, deviceInfo: DeviceInfo? = nil) {
|
|
||||||
self.storage = storage
|
|
||||||
self.deviceInfo = deviceInfo
|
|
||||||
self.shouldConnect = shouldConnect
|
|
||||||
connection.shouldConnectIfPossible = shouldConnect
|
|
||||||
connection.delegate = self
|
|
||||||
}
|
|
||||||
|
|
||||||
func connect() -> Bool {
|
|
||||||
connection.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Published
|
|
||||||
private(set) var deviceState: DeviceState = .disconnected {
|
|
||||||
didSet {
|
|
||||||
log.info("State: \(deviceState)")
|
|
||||||
if case .configured = deviceState {
|
|
||||||
startRegularUpdates()
|
|
||||||
} else {
|
|
||||||
endRegularUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Published
|
|
||||||
private(set) var deviceInfo: DeviceInfo? {
|
|
||||||
didSet {
|
|
||||||
// collectRecordedData()
|
|
||||||
if let deviceInfo, let runningTransfer {
|
|
||||||
runningTransfer.update(info: deviceInfo)
|
|
||||||
let next = runningTransfer.nextRequest()
|
|
||||||
addRequest(next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var openRequests: [BluetoothRequest] = []
|
|
||||||
|
|
||||||
private var runningRequest: BluetoothRequest?
|
|
||||||
|
|
||||||
private var runningTransfer: TemperatureDataTransfer?
|
|
||||||
|
|
||||||
// MARK: Regular updates
|
|
||||||
|
|
||||||
func updateDeviceInfo() {
|
|
||||||
guard case .configured = deviceState else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addRequest(.getInfo)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private var dataUpdateTimer: Timer?
|
|
||||||
|
|
||||||
private func startRegularUpdates() {
|
|
||||||
guard dataUpdateTimer == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.info("Starting updates")
|
|
||||||
dataUpdateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] timer in
|
|
||||||
guard let self = self else {
|
|
||||||
timer.invalidate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.updateDeviceInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
dataUpdateTimer?.fire()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func endRegularUpdates() {
|
|
||||||
guard let dataUpdateTimer else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dataUpdateTimer.invalidate()
|
|
||||||
runningRequest = nil
|
|
||||||
self.dataUpdateTimer = nil
|
|
||||||
log.info("Ending updates")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Requests
|
|
||||||
|
|
||||||
func clearDeviceStorage() {
|
|
||||||
guard let count = deviceInfo?.numberOfRecordedBytes else {
|
|
||||||
log.info("Can't clear device data without device info")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addRequest(.clearRecordingBuffer(byteCount: count))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performNextRequest() {
|
|
||||||
guard runningRequest == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard !openRequests.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let next = openRequests.removeFirst()
|
|
||||||
|
|
||||||
guard connection.send(next.serialized) else {
|
|
||||||
log.warning("Failed to start request \(next)")
|
|
||||||
performNextRequest()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runningRequest = next
|
|
||||||
}
|
|
||||||
|
|
||||||
func addRequest(_ request: BluetoothRequest) {
|
|
||||||
defer {
|
|
||||||
performNextRequest()
|
|
||||||
}
|
|
||||||
let type = request.byte
|
|
||||||
if let runningRequest, runningRequest.byte == type {
|
|
||||||
log.info("Skipping duplicate request \(request)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard !openRequests.contains(where: { $0.byte == type }) else {
|
|
||||||
log.info("Skipping duplicate request \(request)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
openRequests.append(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Data transfer
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func collectRecordedData() -> Bool {
|
|
||||||
guard runningTransfer == nil else {
|
|
||||||
log.info("Transfer already running")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else {
|
|
||||||
log.info("Transfer already scheduled")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard let info = deviceInfo else {
|
|
||||||
log.warning("No device info to start transfer")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard info.numberOfStoredMeasurements > 0 else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let transfer = TemperatureDataTransfer(info: info, previous: storage.lastDeviceTime)
|
|
||||||
runningTransfer = transfer
|
|
||||||
let next = transfer.nextRequest()
|
|
||||||
log.info("Starting transfer")
|
|
||||||
addRequest(next)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didReceive(data: Data, offset: Int, count: Int) {
|
|
||||||
guard let runningTransfer else {
|
|
||||||
log.warning("No running transfer to process device data")
|
|
||||||
self.runningRequest = nil
|
|
||||||
return // TODO: Start new transfer?
|
|
||||||
}
|
|
||||||
guard runningTransfer.add(data: data, offset: offset, count: count) else {
|
|
||||||
self.runningRequest = nil
|
|
||||||
return // TODO: Start new transfer
|
|
||||||
}
|
|
||||||
let next = runningTransfer.nextRequest()
|
|
||||||
addRequest(next)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func decode(info: Data) {
|
|
||||||
guard let newInfo = try? DeviceInfo(info: info) else {
|
|
||||||
log.error("Failed to decode device info")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.deviceInfo = newInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BluetoothClient: DeviceManagerDelegate {
|
|
||||||
|
|
||||||
func deviceManager(shouldConnectToDevice: Bool) {
|
|
||||||
guard !isUpdatingFlag else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.shouldConnect = shouldConnectToDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
func deviceManager(didReceive data: Data) {
|
|
||||||
defer {
|
|
||||||
performNextRequest()
|
|
||||||
}
|
|
||||||
guard let runningRequest else {
|
|
||||||
log.warning("No request active, but \(data) received")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.runningRequest = nil
|
|
||||||
|
|
||||||
guard data.count > 0 else {
|
|
||||||
log.error("No response data for request \(runningRequest)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let type = BluetoothResponseType(rawValue: data[0]) else {
|
|
||||||
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch type {
|
|
||||||
case .success:
|
|
||||||
break
|
|
||||||
case .responseInProgress:
|
|
||||||
log.info("Device is busy for \(runningRequest)")
|
|
||||||
// Retry the request
|
|
||||||
addRequest(runningRequest)
|
|
||||||
return
|
|
||||||
case .invalidNumberOfBytesToDelete:
|
|
||||||
guard case .clearRecordingBuffer = runningRequest else {
|
|
||||||
log.error("Request \(runningRequest) received non-matching response about number of bytes to delete")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If clearing the recording buffer fails due to byte mismatch,
|
|
||||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
|
||||||
addRequest(.getInfo)
|
|
||||||
|
|
||||||
case .responseTooLarge:
|
|
||||||
guard case .getRecordingData = runningRequest else {
|
|
||||||
log.error("Unexpectedly exceeded payload size for request \(runningRequest)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If requesting bytes fails due to the response size,
|
|
||||||
// then requesting new info will update the response size, and the transfer will be resumed
|
|
||||||
addRequest(.getInfo)
|
|
||||||
default:
|
|
||||||
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
|
||||||
// If clearing the recording buffer fails due to byte mismatch,
|
|
||||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
|
||||||
|
|
||||||
addRequest(.getInfo)
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
let payload = data.dropFirst()
|
|
||||||
|
|
||||||
switch runningRequest {
|
|
||||||
case .getInfo:
|
|
||||||
decode(info: payload)
|
|
||||||
case .getRecordingData(let offset, let count):
|
|
||||||
didReceive(data: payload, offset: offset, count: count)
|
|
||||||
case .clearRecordingBuffer:
|
|
||||||
didClearDeviceStorage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didClearDeviceStorage() {
|
|
||||||
guard let runningTransfer else {
|
|
||||||
log.warning("No running transfer after clearing device storage")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer { self.runningTransfer = nil }
|
|
||||||
guard runningTransfer.completeTransfer() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
storage.add(runningTransfer.measurements)
|
|
||||||
storage.lastDeviceTime = runningTransfer.time
|
|
||||||
}
|
|
||||||
|
|
||||||
func deviceManager(didChangeState state: DeviceState) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.deviceState = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
@ -1,67 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
/*
|
|
||||||
enum BluetoothRequest {
|
|
||||||
/**
|
|
||||||
* Request the number of bytes already recorded
|
|
||||||
*
|
|
||||||
* Request:
|
|
||||||
* - No additional bytes expected
|
|
||||||
*
|
|
||||||
* Response:
|
|
||||||
* - `BluetoothResponseType.success`
|
|
||||||
* - the number of recorded bytes as a `Uint16` (2 bytes)
|
|
||||||
* - the number of seconds until the next measurement as a `Uint16` (2 bytes)
|
|
||||||
* - the number of seconds between measurements as a `Uint16` (2 bytes)
|
|
||||||
* - the number of measurements as a `Uint16` (2 bytes)
|
|
||||||
* - the maximum number of bytes to request as a `Uint16` (2 bytes)
|
|
||||||
* - the number of seconds since power on as a `Uint32` (4 bytes)
|
|
||||||
*/
|
|
||||||
case getInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request recording data
|
|
||||||
*
|
|
||||||
* Request:
|
|
||||||
* - Bytes 1-2: Memory offset (`UInt16`)
|
|
||||||
* - Bytes 3-4: Number of bytes (`UInt16`)
|
|
||||||
*
|
|
||||||
* Response:
|
|
||||||
* - `BluetoothResponseType.success`, plus the requested bytes
|
|
||||||
* - `BluetoothResponseType.responseTooLarge` if too many bytes are requested
|
|
||||||
*/
|
|
||||||
case getRecordingData(offset: Int, count: Int)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request deletion of recordings
|
|
||||||
*
|
|
||||||
* Request:
|
|
||||||
* - Bytes 1-2: Number of bytes to clear (uint16_t)
|
|
||||||
*
|
|
||||||
* Response:
|
|
||||||
* - `BluetoothResponseType.success`
|
|
||||||
* - `BluetoothResponseType.invalidNumberOfBytesToDelete`, if the number of bytes does not match.
|
|
||||||
* This may happen when a new temperature recording is performed in between calls
|
|
||||||
*/
|
|
||||||
case clearRecordingBuffer(byteCount: Int)
|
|
||||||
|
|
||||||
var serialized: Data {
|
|
||||||
let firstByte = Data([byte])
|
|
||||||
switch self {
|
|
||||||
case .getInfo:
|
|
||||||
return firstByte
|
|
||||||
case .getRecordingData(let offset, let count):
|
|
||||||
return firstByte + count.twoByteData + offset.twoByteData
|
|
||||||
case .clearRecordingBuffer(let byteCount):
|
|
||||||
return firstByte + byteCount.twoByteData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var byte: UInt8 {
|
|
||||||
switch self {
|
|
||||||
case .getInfo: return 0
|
|
||||||
case .getRecordingData: return 1
|
|
||||||
case .clearRecordingBuffer: return 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
@ -2,15 +2,15 @@ import Foundation
|
|||||||
|
|
||||||
struct DeviceInfo {
|
struct DeviceInfo {
|
||||||
|
|
||||||
/**
|
/// The unique ID generated by the device to distinguish between power cycles
|
||||||
The maximum factor by which the device clock can run
|
let uniqueIdOfPowerCycle: Int
|
||||||
*/
|
|
||||||
private let maximumTimeDilationFactor: Double = 0.01
|
|
||||||
|
|
||||||
|
|
||||||
/// The number of bytes recorded by the tracker
|
/// The number of bytes recorded by the tracker
|
||||||
let numberOfRecordedBytes: Int
|
let numberOfRecordedBytes: Int
|
||||||
|
|
||||||
|
/// The sum of all recorded bytes
|
||||||
|
let dataChecksum: UInt16
|
||||||
|
|
||||||
/// The number of measurements already performed
|
/// The number of measurements already performed
|
||||||
let numberOfStoredMeasurements: Int
|
let numberOfStoredMeasurements: Int
|
||||||
|
|
||||||
@ -46,54 +46,15 @@ struct DeviceInfo {
|
|||||||
time.nextMeasurement.addingTimeInterval(-Double(numberOfStoredMeasurements * measurementInterval))
|
time.nextMeasurement.addingTimeInterval(-Double(numberOfStoredMeasurements * measurementInterval))
|
||||||
}
|
}
|
||||||
|
|
||||||
func estimatedTimeDilation(to previous: DeviceTime?) -> (start: Date, dilation: Double) {
|
|
||||||
let trivialResult = (start: currentMeasurementStartTime, dilation: 1.0)
|
|
||||||
guard let previous else {
|
|
||||||
log.info("No previous device time to compare")
|
|
||||||
return trivialResult
|
|
||||||
}
|
|
||||||
// Check if device was restarted in between
|
|
||||||
guard time.secondsSincePowerOn >= previous.secondsSincePowerOn else {
|
|
||||||
log.info("Device restarted (runtime decreased from \(previous.secondsSincePowerOn) to \(time.secondsSincePowerOn))")
|
|
||||||
return trivialResult
|
|
||||||
}
|
|
||||||
let newMeasurementCount = time.totalNumberOfMeasurements - previous.totalNumberOfMeasurements
|
|
||||||
guard newMeasurementCount >= 0 else {
|
|
||||||
log.info("Device restarted (measurements decreased from \(previous.totalNumberOfMeasurements) to \(time.totalNumberOfMeasurements))")
|
|
||||||
return trivialResult
|
|
||||||
}
|
|
||||||
guard newMeasurementCount > 0 else {
|
|
||||||
log.warning("No new measurements to calculate time difference")
|
|
||||||
return trivialResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that no measurements are missing
|
|
||||||
|
|
||||||
// Calculate the difference between the expected time for the next measurement and the device time
|
|
||||||
let deviceTimeDifference = Double(newMeasurementCount * measurementInterval)
|
|
||||||
let expectedNextMeasurement = previous.nextMeasurement.addingTimeInterval(deviceTimeDifference)
|
|
||||||
let timeDifference = time.nextMeasurement.timeIntervalSince(expectedNextMeasurement)
|
|
||||||
|
|
||||||
let realTimeDifference = time.nextMeasurement.timeIntervalSince(previous.nextMeasurement)
|
|
||||||
let timeDilation = realTimeDifference / deviceTimeDifference
|
|
||||||
|
|
||||||
log.info("Device time dilation \(timeDilation) (difference \(timeDifference))")
|
|
||||||
|
|
||||||
guard abs(timeDilation - 1.0) < maximumTimeDilationFactor else {
|
|
||||||
log.warning("Device time too different from expected value (difference \(timeDifference) s)")
|
|
||||||
return (currentMeasurementStartTime, 1.0)
|
|
||||||
}
|
|
||||||
return (previous.nextMeasurement, timeDilation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DeviceInfo {
|
extension DeviceInfo {
|
||||||
|
|
||||||
init(info: Data) throws {
|
init(info: Data) throws {
|
||||||
let date = Date()
|
let date = Date()
|
||||||
|
|
||||||
var data = info
|
var data = info
|
||||||
|
|
||||||
|
self.uniqueIdOfPowerCycle = try data.decodeFourByteInteger()
|
||||||
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
||||||
let secondsUntilNextMeasurement = try data.decodeTwoByteInteger()
|
let secondsUntilNextMeasurement = try data.decodeTwoByteInteger()
|
||||||
self.measurementInterval = try data.decodeTwoByteInteger()
|
self.measurementInterval = try data.decodeTwoByteInteger()
|
||||||
@ -102,16 +63,24 @@ extension DeviceInfo {
|
|||||||
self.transferBlockSize = try data.decodeTwoByteInteger()
|
self.transferBlockSize = try data.decodeTwoByteInteger()
|
||||||
self.storageSize = try data.decodeTwoByteInteger()
|
self.storageSize = try data.decodeTwoByteInteger()
|
||||||
let secondsSincePowerOn = try data.decodeFourByteInteger()
|
let secondsSincePowerOn = try data.decodeFourByteInteger()
|
||||||
|
let startSecondsOfCurrentRecording = try data.decodeFourByteInteger()
|
||||||
|
self.dataChecksum = try data.decodeUInt16()
|
||||||
|
self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED
|
||||||
|
|
||||||
|
self.sensor0 = try data.decodeSensor()
|
||||||
|
self.sensor1 = try data.decodeSensor()
|
||||||
|
|
||||||
|
guard data.isEmpty else {
|
||||||
|
log.error("\(data.count) bytes remaining in device info buffer")
|
||||||
|
throw DeviceInfoError.missingData
|
||||||
|
}
|
||||||
|
|
||||||
self.time = .init(
|
self.time = .init(
|
||||||
date: date,
|
date: date,
|
||||||
secondsSincePowerOn: secondsSincePowerOn,
|
secondsSincePowerOn: secondsSincePowerOn,
|
||||||
totalNumberOfMeasurements: totalNumberOfMeasurements,
|
totalNumberOfMeasurements: totalNumberOfMeasurements,
|
||||||
secondsUntilNextMeasurement: secondsUntilNextMeasurement)
|
secondsUntilNextMeasurement: secondsUntilNextMeasurement,
|
||||||
let _ = try data.decodeFourByteInteger()
|
secondsOfFirstMeasurement: startSecondsOfCurrentRecording)
|
||||||
self.sensor0 = try data.decodeSensor()
|
|
||||||
self.sensor1 = try data.decodeSensor()
|
|
||||||
self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +88,9 @@ extension DeviceInfo {
|
|||||||
|
|
||||||
static var mock: DeviceInfo {
|
static var mock: DeviceInfo {
|
||||||
.init(
|
.init(
|
||||||
|
uniqueIdOfPowerCycle: .random(in: 0...Int(UInt32.max)),
|
||||||
numberOfRecordedBytes: 123,
|
numberOfRecordedBytes: 123,
|
||||||
|
dataChecksum: .random(in: .min...UInt16.max),
|
||||||
numberOfStoredMeasurements: 234,
|
numberOfStoredMeasurements: 234,
|
||||||
measurementInterval: 60,
|
measurementInterval: 60,
|
||||||
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)),
|
||||||
@ -130,3 +101,7 @@ extension DeviceInfo {
|
|||||||
transferBlockSize: 180)
|
transferBlockSize: 180)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension DeviceInfo: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,258 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CoreBluetooth
|
|
||||||
|
|
||||||
final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|
||||||
|
|
||||||
static let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
|
|
||||||
|
|
||||||
static let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")
|
|
||||||
|
|
||||||
private var manager: CBCentralManager! = nil
|
|
||||||
|
|
||||||
private(set) var lastRSSI: Int = 0
|
|
||||||
|
|
||||||
weak var delegate: DeviceManagerDelegate?
|
|
||||||
|
|
||||||
var state: DeviceState = .disconnected {
|
|
||||||
didSet {
|
|
||||||
delegate?.deviceManager(didChangeState: state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
self.manager = CBCentralManager(delegate: self, queue: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func connect() -> Bool {
|
|
||||||
switch state {
|
|
||||||
case .bluetoothDisabled:
|
|
||||||
log.info("Can't connect, bluetooth disabled")
|
|
||||||
return false
|
|
||||||
case .disconnected:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
guard !manager.isScanning else {
|
|
||||||
state = .scanning
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if !shouldConnectIfPossible {
|
|
||||||
shouldConnectIfPossible = true
|
|
||||||
}
|
|
||||||
state = .scanning
|
|
||||||
manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID])
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var shouldConnectIfPossible = true {
|
|
||||||
didSet {
|
|
||||||
updateConnectionOnChange()
|
|
||||||
delegate?.deviceManager(shouldConnectToDevice: shouldConnectIfPossible)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateConnectionOnChange() {
|
|
||||||
if shouldConnectIfPossible {
|
|
||||||
ensureConnection()
|
|
||||||
} else {
|
|
||||||
disconnectIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func ensureConnection() {
|
|
||||||
switch state {
|
|
||||||
case .disconnected:
|
|
||||||
connect()
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func disconnectIfNeeded() {
|
|
||||||
switch state {
|
|
||||||
case .bluetoothDisabled, .disconnected:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func disconnect() {
|
|
||||||
if shouldConnectIfPossible {
|
|
||||||
shouldConnectIfPossible = false
|
|
||||||
}
|
|
||||||
switch state {
|
|
||||||
case .bluetoothDisabled, .disconnected:
|
|
||||||
return
|
|
||||||
case .scanning:
|
|
||||||
manager.stopScan()
|
|
||||||
state = .disconnected
|
|
||||||
return
|
|
||||||
case .connecting(let device),
|
|
||||||
.discoveringCharacteristic(let device),
|
|
||||||
.discoveringServices(device: let device),
|
|
||||||
.configured(let device, _):
|
|
||||||
manager.cancelPeripheralConnection(device)
|
|
||||||
manager.stopScan()
|
|
||||||
state = .disconnected
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func send(_ data: Data) -> Bool {
|
|
||||||
guard case .configured(let device, let characteristic) = state else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
device.writeValue(data, for: characteristic, type: .withResponse)
|
|
||||||
return self.read()
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
private func read() -> Bool {
|
|
||||||
guard case .configured(let device, let characteristic) = state else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
device.readValue(for: characteristic)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
|
||||||
guard shouldConnectIfPossible else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
peripheral.delegate = self
|
|
||||||
manager.connect(peripheral)
|
|
||||||
manager.stopScan()
|
|
||||||
state = .connecting(device: peripheral)
|
|
||||||
}
|
|
||||||
|
|
||||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
||||||
switch central.state {
|
|
||||||
case .poweredOff:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
case .poweredOn:
|
|
||||||
state = .disconnected
|
|
||||||
connect()
|
|
||||||
case .unsupported:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth is not supported")
|
|
||||||
case .unknown:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth state is unknown")
|
|
||||||
case .resetting:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth is resetting")
|
|
||||||
case .unauthorized:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth is not authorized")
|
|
||||||
@unknown default:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.warning("Unknown state \(central.state)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
||||||
log.info("Connected to " + peripheral.name!)
|
|
||||||
peripheral.discoverServices([DeviceManager.serviceUUID])
|
|
||||||
state = .discoveringServices(device: peripheral)
|
|
||||||
}
|
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
||||||
log.info("Disconnected from " + peripheral.name!)
|
|
||||||
state = .disconnected
|
|
||||||
// Attempt to reconnect
|
|
||||||
if shouldConnectIfPossible {
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
||||||
log.warning("Failed to connect device '\(peripheral.name ?? "NO_NAME")'")
|
|
||||||
if let error = error {
|
|
||||||
log.warning(error.localizedDescription)
|
|
||||||
}
|
|
||||||
state = manager.isScanning ? .scanning : .disconnected
|
|
||||||
// Attempt to reconnect
|
|
||||||
if shouldConnectIfPossible {
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DeviceManager: CBPeripheralDelegate {
|
|
||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
||||||
guard let services = peripheral.services, !services.isEmpty else {
|
|
||||||
log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
|
|
||||||
log.error("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
peripheral.discoverCharacteristics([DeviceManager.characteristicUUID], for: service)
|
|
||||||
state = .discoveringCharacteristic(device: peripheral)
|
|
||||||
}
|
|
||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.error("Failed to discover characteristics: \(error)")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
|
|
||||||
log.error("No characteristics found for device")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for characteristic in characteristics {
|
|
||||||
guard characteristic.uuid == DeviceManager.characteristicUUID else {
|
|
||||||
log.warning("Unused characteristic \(characteristic.uuid.uuidString)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
state = .configured(device: peripheral, characteristic: characteristic)
|
|
||||||
peripheral.setNotifyValue(true, for: characteristic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.warning("Failed to get RSSI: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastRSSI = RSSI.intValue
|
|
||||||
log.info("RSSI: \(lastRSSI)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.error("Failed to read value update: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard case .configured(device: _, characteristic: let storedCharacteristic) = state else {
|
|
||||||
log.warning("Received data while not properly configured")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard characteristic.uuid == storedCharacteristic.uuid else {
|
|
||||||
log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let data = characteristic.value else {
|
|
||||||
log.warning("No data")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delegate?.deviceManager(didReceive: data)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
protocol DeviceManagerDelegate: AnyObject {
|
|
||||||
|
|
||||||
func deviceManager(shouldConnectToDevice: Bool)
|
|
||||||
|
|
||||||
func deviceManager(didReceive data: Data)
|
|
||||||
|
|
||||||
func deviceManager(didChangeState state: DeviceState)
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CoreBluetooth
|
|
||||||
|
|
||||||
enum DeviceState {
|
|
||||||
|
|
||||||
case bluetoothDisabled
|
|
||||||
|
|
||||||
case scanning
|
|
||||||
|
|
||||||
case connecting(device: CBPeripheral)
|
|
||||||
|
|
||||||
case discoveringServices(device: CBPeripheral)
|
|
||||||
|
|
||||||
case discoveringCharacteristic(device: CBPeripheral)
|
|
||||||
|
|
||||||
case configured(device: CBPeripheral, characteristic: CBCharacteristic)
|
|
||||||
|
|
||||||
case disconnected
|
|
||||||
|
|
||||||
var text: String {
|
|
||||||
switch self {
|
|
||||||
case .bluetoothDisabled:
|
|
||||||
return "Bluetooth is disabled"
|
|
||||||
case .scanning:
|
|
||||||
return "Scanning..."
|
|
||||||
case .connecting(let device):
|
|
||||||
guard let name = device.name else {
|
|
||||||
return "Connecting..."
|
|
||||||
}
|
|
||||||
return "Connecting to \(name)..."
|
|
||||||
case .discoveringServices:
|
|
||||||
return "Discovering service..."
|
|
||||||
case .discoveringCharacteristic:
|
|
||||||
return "Discovering characteristic..."
|
|
||||||
case .configured(let device, _):
|
|
||||||
guard let name = device.name else {
|
|
||||||
return "Connected"
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
case .disconnected:
|
|
||||||
return "Not connected"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var device: CBPeripheral? {
|
|
||||||
switch self {
|
|
||||||
case .bluetoothDisabled, .disconnected, .scanning:
|
|
||||||
return nil
|
|
||||||
case .connecting(let device),
|
|
||||||
.discoveringCharacteristic(let device),
|
|
||||||
.discoveringServices(device: let device),
|
|
||||||
.configured(let device, _):
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DeviceState: CustomStringConvertible {
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .bluetoothDisabled:
|
|
||||||
return "Bluetooth disabled"
|
|
||||||
case .scanning:
|
|
||||||
return "Searching for device"
|
|
||||||
case .connecting:
|
|
||||||
return "Connecting to device"
|
|
||||||
case .discoveringServices:
|
|
||||||
return "Discovering services"
|
|
||||||
case .discoveringCharacteristic:
|
|
||||||
return "Discovering characteristics"
|
|
||||||
case .configured:
|
|
||||||
return "Connected"
|
|
||||||
case .disconnected:
|
|
||||||
return "Disconnected"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DeviceState: Equatable {
|
|
||||||
|
|
||||||
}
|
|
@ -10,6 +10,8 @@ struct DeviceTime {
|
|||||||
|
|
||||||
let secondsUntilNextMeasurement: Int
|
let secondsUntilNextMeasurement: Int
|
||||||
|
|
||||||
|
let secondsOfFirstMeasurement: Int
|
||||||
|
|
||||||
var nextMeasurement: Date {
|
var nextMeasurement: Date {
|
||||||
date.adding(seconds: secondsUntilNextMeasurement)
|
date.adding(seconds: secondsUntilNextMeasurement)
|
||||||
}
|
}
|
||||||
@ -47,6 +49,7 @@ extension DeviceTime: Codable {
|
|||||||
self.secondsSincePowerOn = try container.decode(Int.self)
|
self.secondsSincePowerOn = try container.decode(Int.self)
|
||||||
self.totalNumberOfMeasurements = try container.decode(Int.self)
|
self.totalNumberOfMeasurements = try container.decode(Int.self)
|
||||||
self.secondsUntilNextMeasurement = try container.decode(Int.self)
|
self.secondsUntilNextMeasurement = try container.decode(Int.self)
|
||||||
|
self.secondsOfFirstMeasurement = try container.decode(Int.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
@ -55,6 +58,7 @@ extension DeviceTime: Codable {
|
|||||||
try container.encode(secondsSincePowerOn)
|
try container.encode(secondsSincePowerOn)
|
||||||
try container.encode(totalNumberOfMeasurements)
|
try container.encode(totalNumberOfMeasurements)
|
||||||
try container.encode(secondsUntilNextMeasurement)
|
try container.encode(secondsUntilNextMeasurement)
|
||||||
|
try container.encode(secondsOfFirstMeasurement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +69,7 @@ extension DeviceTime {
|
|||||||
date: .now,
|
date: .now,
|
||||||
secondsSincePowerOn: 125,
|
secondsSincePowerOn: 125,
|
||||||
totalNumberOfMeasurements: 3,
|
totalNumberOfMeasurements: 3,
|
||||||
secondsUntilNextMeasurement: 55)
|
secondsUntilNextMeasurement: 55,
|
||||||
|
secondsOfFirstMeasurement: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,3 +75,7 @@ extension DeviceWakeCause {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension DeviceWakeCause: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,18 +1,29 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
|
|
||||||
|
protocol BluetoothDeviceDelegate: AnyObject {
|
||||||
|
|
||||||
|
func bluetoothDevice(didUpdate info: DeviceInfo?)
|
||||||
|
}
|
||||||
|
|
||||||
actor BluetoothDevice: NSObject, ObservableObject {
|
actor BluetoothDevice: NSObject, ObservableObject {
|
||||||
|
|
||||||
private let peripheral: CBPeripheral!
|
let peripheral: CBPeripheral!
|
||||||
|
|
||||||
private let characteristic: CBCharacteristic!
|
private let characteristic: CBCharacteristic!
|
||||||
|
|
||||||
@MainActor @Published
|
@Published
|
||||||
var lastDeviceInfo: DeviceInfo?
|
var lastDeviceInfo: DeviceInfo?
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
private(set) var lastRSSI: Int = 0
|
private(set) var lastRSSI: Int = 0
|
||||||
|
|
||||||
|
weak var delegate: BluetoothDeviceDelegate?
|
||||||
|
|
||||||
|
func set(delegate: BluetoothDeviceDelegate?) {
|
||||||
|
self.delegate = delegate
|
||||||
|
}
|
||||||
|
|
||||||
init(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
|
init(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
|
||||||
self.peripheral = peripheral
|
self.peripheral = peripheral
|
||||||
self.characteristic = characteristic
|
self.characteristic = characteristic
|
||||||
@ -33,9 +44,10 @@ actor BluetoothDevice: NSObject, ObservableObject {
|
|||||||
guard let info = await getInfo() else {
|
guard let info = await getInfo() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Task { @MainActor in
|
lastDeviceInfo = info
|
||||||
lastDeviceInfo = info
|
delegate?.bluetoothDevice(didUpdate: info)
|
||||||
}
|
#warning("Don't use global variable")
|
||||||
|
storage.save(deviceInfo: info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInfo() async -> DeviceInfo? {
|
func getInfo() async -> DeviceInfo? {
|
||||||
|
@ -26,33 +26,56 @@ final class BluetoothScanner: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
@Published
|
@Published
|
||||||
var configuredDevice: BluetoothDevice?
|
var configuredDevice: BluetoothDevice?
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var lastDeviceInfo: DeviceInfo?
|
||||||
|
|
||||||
private var connectingDevice: CBPeripheral?
|
private var connectingDevice: CBPeripheral?
|
||||||
|
|
||||||
var isScanningForDevices: Bool {
|
@Published
|
||||||
get {
|
var isScanningForDevices: Bool = false {
|
||||||
manager.isScanning
|
didSet {
|
||||||
}
|
if isScanningForDevices {
|
||||||
set {
|
startScanning()
|
||||||
if newValue {
|
|
||||||
guard !manager.isScanning else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager.scanForPeripherals(withServices: [serviceUUID])
|
|
||||||
log.info("Scanner: Started scanning for devices")
|
|
||||||
} else {
|
} else {
|
||||||
guard manager.isScanning else {
|
stopScanning()
|
||||||
return
|
|
||||||
}
|
|
||||||
manager.stopScan()
|
|
||||||
log.info("Scanner: Stopped scanning for devices")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startScanning() {
|
||||||
|
guard !manager.isScanning else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.scanForPeripherals(withServices: [serviceUUID])
|
||||||
|
log.info("Scanner: Started scanning for devices")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopScanning() {
|
||||||
|
guard manager.isScanning else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.stopScan()
|
||||||
|
log.info("Scanner: Stopped scanning for devices")
|
||||||
|
}
|
||||||
|
|
||||||
|
var isConnectingOrConnected: Bool {
|
||||||
|
configuredDevice != nil || connectingDevice != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect() {
|
||||||
|
if let configuredDevice {
|
||||||
|
manager.cancelPeripheralConnection(configuredDevice.peripheral)
|
||||||
|
}
|
||||||
|
if let connectingDevice {
|
||||||
|
manager.cancelPeripheralConnection(connectingDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
connectionState = .noDeviceFound
|
connectionState = .noDeviceFound
|
||||||
super.init()
|
super.init()
|
||||||
self.manager = CBCentralManager(delegate: self, queue: nil)
|
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||||||
|
self.isScanningForDevices = manager.isScanning
|
||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||||
@ -95,6 +118,7 @@ final class BluetoothScanner: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
peripheral.discoverServices([serviceUUID])
|
peripheral.discoverServices([serviceUUID])
|
||||||
connectingDevice = peripheral
|
connectingDevice = peripheral
|
||||||
configuredDevice = nil
|
configuredDevice = nil
|
||||||
|
isScanningForDevices = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||||
@ -121,13 +145,15 @@ extension BluetoothScanner: CBPeripheralDelegate {
|
|||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
connectionState = .noDeviceFound
|
connectionState = .noDeviceFound
|
||||||
connectingDevice = nil
|
connectingDevice = nil
|
||||||
|
isScanningForDevices = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
|
guard let service = services.first(where: { $0.uuid.uuidString == serviceUUID.uuidString }) else {
|
||||||
log.error("Connected device '\(peripheral.name ?? "No Name")': Required service not found: \(services.map { $0.uuid.uuidString})")
|
log.error("Connected device '\(peripheral.name ?? "No Name")': Required service not found: \(services.map { $0.uuid.uuidString})")
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
connectionState = .noDeviceFound
|
connectionState = .noDeviceFound
|
||||||
connectingDevice = nil
|
connectingDevice = nil
|
||||||
|
isScanningForDevices = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
peripheral.delegate = self
|
peripheral.delegate = self
|
||||||
@ -142,6 +168,7 @@ extension BluetoothScanner: CBPeripheralDelegate {
|
|||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
connectionState = .noDeviceFound
|
connectionState = .noDeviceFound
|
||||||
connectingDevice = nil
|
connectingDevice = nil
|
||||||
|
isScanningForDevices = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +177,7 @@ extension BluetoothScanner: CBPeripheralDelegate {
|
|||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
connectionState = .noDeviceFound
|
connectionState = .noDeviceFound
|
||||||
connectingDevice = nil
|
connectingDevice = nil
|
||||||
|
isScanningForDevices = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,9 +196,22 @@ extension BluetoothScanner: CBPeripheralDelegate {
|
|||||||
guard let desiredCharacteristic else {
|
guard let desiredCharacteristic else {
|
||||||
log.error("Connected device '\(peripheral.name ?? "No Name")': Characteristic not found")
|
log.error("Connected device '\(peripheral.name ?? "No Name")': Characteristic not found")
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
isScanningForDevices = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
|
configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
|
||||||
|
Task {
|
||||||
|
await configuredDevice?.set(delegate: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BluetoothScanner: BluetoothDeviceDelegate {
|
||||||
|
|
||||||
|
func bluetoothDevice(didUpdate info: DeviceInfo?) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.lastDeviceInfo = info
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,405 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CoreBluetooth
|
|
||||||
/*
|
|
||||||
actor DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObject {
|
|
||||||
|
|
||||||
static let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
|
|
||||||
|
|
||||||
static let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")
|
|
||||||
|
|
||||||
private var manager: CBCentralManager! = nil
|
|
||||||
|
|
||||||
private(set) var lastRSSI: Int = 0 // TODO: Provide function to update
|
|
||||||
|
|
||||||
@Published
|
|
||||||
var state: DeviceState = .disconnected
|
|
||||||
|
|
||||||
var isConnected: Bool {
|
|
||||||
// Automatically updates with device state
|
|
||||||
if case .configured = state {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
self.manager = CBCentralManager(delegate: self, queue: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Allow the client to scan for devices and connect to the first device found with the correct characteristic
|
|
||||||
*/
|
|
||||||
@discardableResult
|
|
||||||
func initiateDeviceConnection() -> Bool {
|
|
||||||
switch state {
|
|
||||||
case .bluetoothDisabled:
|
|
||||||
log.info("Can't connect, bluetooth disabled")
|
|
||||||
return false
|
|
||||||
case .disconnected:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
guard !manager.isScanning else {
|
|
||||||
state = .scanning
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
shouldConnectIfPossible = true
|
|
||||||
state = .scanning
|
|
||||||
manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID])
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func updateRSSIForConnectedDevice() -> Bool {
|
|
||||||
guard let device = state.device else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
device.readRSSI()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Indicate that a connection should be attempted when found.
|
|
||||||
|
|
||||||
This does not necessarily indicate that the phone is scanning for devices.
|
|
||||||
*/
|
|
||||||
@Published
|
|
||||||
var shouldConnectIfPossible = true {
|
|
||||||
didSet {
|
|
||||||
guard oldValue != shouldConnectIfPossible else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateConnectionOnChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateConnectionOnChange() {
|
|
||||||
if shouldConnectIfPossible {
|
|
||||||
ensureConnection()
|
|
||||||
} else {
|
|
||||||
disconnectIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func ensureConnection() {
|
|
||||||
switch state {
|
|
||||||
case .disconnected:
|
|
||||||
initiateDeviceConnection()
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func disconnectIfNeeded() {
|
|
||||||
switch state {
|
|
||||||
case .bluetoothDisabled, .disconnected:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func disconnect() {
|
|
||||||
shouldConnectIfPossible = false
|
|
||||||
switch state {
|
|
||||||
case .bluetoothDisabled, .disconnected:
|
|
||||||
return
|
|
||||||
case .scanning:
|
|
||||||
manager.stopScan()
|
|
||||||
state = .disconnected
|
|
||||||
return
|
|
||||||
case .connecting(let device),
|
|
||||||
.discoveringCharacteristic(let device),
|
|
||||||
.discoveringServices(device: let device),
|
|
||||||
.configured(let device, _):
|
|
||||||
manager.cancelPeripheralConnection(device)
|
|
||||||
manager.stopScan()
|
|
||||||
state = .disconnected
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var requestContinuation: CheckedContinuation<Data?, Never>?
|
|
||||||
|
|
||||||
func getInfo() async -> DeviceInfo? {
|
|
||||||
await get(DeviceInfoRequest())
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDeviceData(offset: Int, count: Int) async -> Data? {
|
|
||||||
await get(DeviceDataRequest(offset: offset, count: count))
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteDeviceData(byteCount: Int) async -> Bool {
|
|
||||||
await get(DeviceDataResetRequest(byteCount: byteCount)) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func get<Request>(_ request: Request) async -> Request.Response? where Request: DeviceRequest {
|
|
||||||
guard requestContinuation == nil else {
|
|
||||||
// Prevent parallel requests
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard case .configured(let device, let characteristic) = state else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let requestData = Data([request.type.rawValue]) + request.payload
|
|
||||||
let responseData: Data? = await withCheckedContinuation { continuation in
|
|
||||||
requestContinuation = continuation
|
|
||||||
device.writeValue(requestData, for: characteristic, type: .withResponse)
|
|
||||||
device.readValue(for: characteristic)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
|
|
||||||
Task {
|
|
||||||
await self?.checkTimeoutForCurrentRequest(request.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let responseData else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let responseCode = responseData.first else {
|
|
||||||
log.error("Request \(request.type) got response of zero bytes")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let responseType = BluetoothResponseType(rawValue: responseCode) else {
|
|
||||||
log.error("Request \(request.type) got unknown response code \(responseCode)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
switch responseType {
|
|
||||||
case .success, .responseTooLarge, .invalidNumberOfBytesToDelete:
|
|
||||||
break
|
|
||||||
case .invalidCommand:
|
|
||||||
log.error("Request \(request.type) failed: Invalid command")
|
|
||||||
return nil
|
|
||||||
case .unknownCommand:
|
|
||||||
log.error("Request \(request.type) failed: Unknown command")
|
|
||||||
return nil
|
|
||||||
case .responseInProgress:
|
|
||||||
log.info("Request \(request.type) failed: Device is busy")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return request.makeResponse(from: responseData.dropFirst(), responseType: responseType)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func checkTimeoutForCurrentRequest(_ type: BluetoothRequestType) {
|
|
||||||
guard let requestContinuation else { return }
|
|
||||||
log.info("Timed out for request \(type)")
|
|
||||||
requestContinuation.resume(returning: nil)
|
|
||||||
self.requestContinuation = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
|
||||||
Task {
|
|
||||||
await didDiscover(peripheral: peripheral)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didDiscover(peripheral: CBPeripheral) {
|
|
||||||
guard shouldConnectIfPossible else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
peripheral.delegate = self
|
|
||||||
manager.connect(peripheral)
|
|
||||||
manager.stopScan()
|
|
||||||
state = .connecting(device: peripheral)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
||||||
Task {
|
|
||||||
await didUpdate(state: central.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didUpdate(state newState: CBManagerState) {
|
|
||||||
switch newState {
|
|
||||||
case .poweredOff:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
case .poweredOn:
|
|
||||||
state = .disconnected
|
|
||||||
initiateDeviceConnection()
|
|
||||||
case .unsupported:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth is not supported")
|
|
||||||
case .unknown:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth state is unknown")
|
|
||||||
case .resetting:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth is resetting")
|
|
||||||
case .unauthorized:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.info("Bluetooth is not authorized")
|
|
||||||
@unknown default:
|
|
||||||
state = .bluetoothDisabled
|
|
||||||
log.warning("Unknown state \(newState)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
||||||
Task {
|
|
||||||
await didConnect(to: peripheral)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didConnect(to peripheral: CBPeripheral) {
|
|
||||||
log.info("Connected to " + peripheral.name!)
|
|
||||||
peripheral.discoverServices([DeviceManager.serviceUUID])
|
|
||||||
state = .discoveringServices(device: peripheral)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
||||||
Task {
|
|
||||||
await didDisconnect(from: peripheral, error: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didDisconnect(from peripheral: CBPeripheral, error: Error?) {
|
|
||||||
log.info("Disconnected from " + peripheral.name!)
|
|
||||||
state = .disconnected
|
|
||||||
// Attempt to reconnect
|
|
||||||
if shouldConnectIfPossible {
|
|
||||||
initiateDeviceConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
||||||
Task {
|
|
||||||
await didFailToConnect(to: peripheral, error: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didFailToConnect(to peripheral: CBPeripheral, error: Error?) {
|
|
||||||
log.warning("Failed to connect device '\(peripheral.name ?? "NO_NAME")'")
|
|
||||||
if let error = error {
|
|
||||||
log.warning(error.localizedDescription)
|
|
||||||
}
|
|
||||||
state = manager.isScanning ? .scanning : .disconnected
|
|
||||||
// Attempt to reconnect
|
|
||||||
if shouldConnectIfPossible {
|
|
||||||
initiateDeviceConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DeviceConnection: CBPeripheralDelegate {
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
||||||
Task {
|
|
||||||
await didDiscoverServices(for: peripheral)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didDiscoverServices(for peripheral: CBPeripheral) {
|
|
||||||
guard let services = peripheral.services, !services.isEmpty else {
|
|
||||||
log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
|
|
||||||
log.error("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
peripheral.discoverCharacteristics([DeviceManager.characteristicUUID], for: service)
|
|
||||||
state = .discoveringCharacteristic(device: peripheral)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
||||||
Task {
|
|
||||||
await didDiscoverCharacteristics(for: service, of: peripheral, error: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didDiscoverCharacteristics(for service: CBService, of peripheral: CBPeripheral, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.error("Failed to discover characteristics: \(error)")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
|
|
||||||
log.error("No characteristics found for device")
|
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for characteristic in characteristics {
|
|
||||||
guard characteristic.uuid == DeviceManager.characteristicUUID else {
|
|
||||||
log.warning("Unused characteristic \(characteristic.uuid.uuidString)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
state = .configured(device: peripheral, characteristic: characteristic)
|
|
||||||
peripheral.setNotifyValue(true, for: characteristic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.warning("Failed to get RSSI: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Task {
|
|
||||||
await update(rssi: RSSI.intValue)
|
|
||||||
}
|
|
||||||
log.info("RSSI: \(RSSI.intValue)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func update(rssi: Int) {
|
|
||||||
lastRSSI = rssi
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
||||||
Task {
|
|
||||||
await didUpdateValue(for: characteristic, of: peripheral, error: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didUpdateValue(for characteristic: CBCharacteristic, of peripheral: CBPeripheral, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
log.error("Failed to read value update: \(error)")
|
|
||||||
continueRequest(with: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard case .configured(device: _, characteristic: let storedCharacteristic) = state else {
|
|
||||||
log.warning("Received data while not properly configured")
|
|
||||||
continueRequest(with: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard characteristic.uuid == storedCharacteristic.uuid else {
|
|
||||||
log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)")
|
|
||||||
continueRequest(with: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let data = characteristic.value else {
|
|
||||||
log.warning("No data")
|
|
||||||
continueRequest(with: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continueRequest(with: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func continueRequest(with response: Data?) {
|
|
||||||
guard let requestContinuation else {
|
|
||||||
log.error("No continuation to handle request data (\(response?.count ?? 0) bytes)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
requestContinuation.resume(returning: response)
|
|
||||||
self.requestContinuation = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
96
TempTrack/Connection/TransferHandler.swift
Normal file
96
TempTrack/Connection/TransferHandler.swift
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TransferHandler: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var bytesTransferred: Double = 0.0
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var totalBytes: Double = 0.0
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var measurements: [TemperatureMeasurement] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var transferIsRunning = false
|
||||||
|
|
||||||
|
func startTransfer(from device: BluetoothDevice, with info: DeviceInfo, storage: PersistentStorage) {
|
||||||
|
#warning("Update device info during transfer")
|
||||||
|
discardTransfer()
|
||||||
|
transferIsRunning = true
|
||||||
|
let total = info.numberOfRecordedBytes
|
||||||
|
let chunkSize = info.transferBlockSize
|
||||||
|
totalBytes = Double(total)
|
||||||
|
Task {
|
||||||
|
defer {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.transferIsRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var data = Data(capacity: total)
|
||||||
|
while data.count < total {
|
||||||
|
let remainingBytes = total - data.count
|
||||||
|
let currentChunkSize = min(remainingBytes, chunkSize)
|
||||||
|
guard let chunk = await device.getDeviceData(offset: data.count, count: currentChunkSize) else {
|
||||||
|
log.warning("Failed to finish transfer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !chunk.isEmpty else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data.append(chunk)
|
||||||
|
let count = Double(data.count)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.bytesTransferred = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.count != info.numberOfRecordedBytes {
|
||||||
|
log.warning("Expected \(info.numberOfRecordedBytes) in transfer, got only \(data.count)")
|
||||||
|
}
|
||||||
|
let sum = data.reduce(0) { $0 &+ UInt16($1) }
|
||||||
|
if sum != info.dataChecksum {
|
||||||
|
log.warning("Checksum does not match")
|
||||||
|
}
|
||||||
|
if data.count != 4 * info.numberOfStoredMeasurements {
|
||||||
|
log.warning("expected \(4 * info.numberOfStoredMeasurements) bytes for \(info.numberOfStoredMeasurements) measurements, got \(data.count)")
|
||||||
|
}
|
||||||
|
storage.saveTransferData(data: data, date: info.time.date)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.bytesTransferred = self.totalBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
let recordingStart = info.currentMeasurementStartTime
|
||||||
|
while !data.isEmpty {
|
||||||
|
guard data.count >= 4 else {
|
||||||
|
log.error("Expected four bytes at index \(total - data.count - 1)")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let intervalCount = try! data.decodeUInt16()
|
||||||
|
let temp0 = TemperatureValue(byte: data.removeFirst())
|
||||||
|
let temp1 = TemperatureValue(byte: data.removeFirst())
|
||||||
|
let date = recordingStart
|
||||||
|
.addingTimeInterval(TimeInterval(intervalCount) * TimeInterval(info.measurementInterval))
|
||||||
|
let measurement = TemperatureMeasurement(
|
||||||
|
sensor0: temp0,
|
||||||
|
sensor1: temp1,
|
||||||
|
date: date)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.measurements.append(measurement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveTransfer(in storage: PersistentStorage) {
|
||||||
|
storage.add(measurements)
|
||||||
|
discardTransfer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func discardTransfer() {
|
||||||
|
self.measurements = []
|
||||||
|
self.bytesTransferred = 0
|
||||||
|
self.totalBytes = 0
|
||||||
|
}
|
||||||
|
}
|
@ -13,12 +13,15 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private let disconnectedColor = Color(white: 0.8)
|
private let disconnectedColor = Color(white: 0.8)
|
||||||
|
|
||||||
@StateObject
|
@ObservedObject
|
||||||
var scanner = BluetoothScanner()
|
var scanner: BluetoothScanner
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var storage: PersistentStorage
|
var storage: PersistentStorage
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var transfer: TransferHandler
|
||||||
|
|
||||||
@State
|
@State
|
||||||
var showDeviceInfo = false
|
var showDeviceInfo = false
|
||||||
|
|
||||||
@ -33,14 +36,12 @@ struct ContentView: View {
|
|||||||
|
|
||||||
@State
|
@State
|
||||||
var deviceInfoUpdateTimer: Timer?
|
var deviceInfoUpdateTimer: Timer?
|
||||||
|
|
||||||
init() { }
|
|
||||||
|
|
||||||
var averageTemperature: Double? {
|
var averageTemperature: Double? {
|
||||||
guard let bluetoothDevice = scanner.configuredDevice else {
|
guard scanner.configuredDevice != nil else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
guard let info = bluetoothDevice.lastDeviceInfo else {
|
guard let info = scanner.lastDeviceInfo else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let t1 = info.sensor1?.optionalValue
|
let t1 = info.sensor1?.optionalValue
|
||||||
@ -123,7 +124,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hasNoDeviceInfo: Bool {
|
var hasNoDeviceInfo: Bool {
|
||||||
scanner.configuredDevice?.lastDeviceInfo == nil
|
scanner.lastDeviceInfo == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var isDisconnected: Bool {
|
var isDisconnected: Bool {
|
||||||
@ -155,6 +156,7 @@ struct ContentView: View {
|
|||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
if storage.recentMeasurements.isEmpty {
|
if storage.recentMeasurements.isEmpty {
|
||||||
Text("No recent measurements")
|
Text("No recent measurements")
|
||||||
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,20 +169,26 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
self.scanner.isScanningForDevices.toggle()
|
if scanner.isScanningForDevices {
|
||||||
|
scanner.isScanningForDevices = false
|
||||||
|
} else if scanner.isConnectingOrConnected {
|
||||||
|
scanner.disconnect()
|
||||||
|
} else {
|
||||||
|
scanner.isScanningForDevices = true
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemSymbol: connectionSymbol)
|
Image(systemSymbol: connectionSymbol)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
Spacer()
|
Spacer()
|
||||||
if let device = scanner.configuredDevice {
|
if scanner.lastDeviceInfo != nil {
|
||||||
Button {
|
Button {
|
||||||
self.showDeviceInfo = true
|
self.showDeviceInfo = true
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemSymbol: .infoCircle)
|
Image(systemSymbol: .infoCircle)
|
||||||
.foregroundColor(device.lastDeviceInfo == nil ? .gray : .white)
|
.foregroundColor(.white)
|
||||||
}.disabled(device.lastDeviceInfo == nil)
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
showDataTransferView = true
|
showDataTransferView = true
|
||||||
@ -195,8 +203,6 @@ struct ContentView: View {
|
|||||||
Image(systemSymbol: .arrowUpArrowDownCircle)
|
Image(systemSymbol: .arrowUpArrowDownCircle)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.font(.system(size: 30, weight: .light))
|
.font(.system(size: 30, weight: .light))
|
||||||
@ -204,7 +210,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.sheet(isPresented: $showDeviceInfo) {
|
.sheet(isPresented: $showDeviceInfo) {
|
||||||
if let info = scanner.configuredDevice?.lastDeviceInfo {
|
if let info = scanner.lastDeviceInfo {
|
||||||
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
|
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
@ -217,12 +223,14 @@ struct ContentView: View {
|
|||||||
.sheet(isPresented: $showLog) {
|
.sheet(isPresented: $showLog) {
|
||||||
LogView()
|
LogView()
|
||||||
.environmentObject(log)
|
.environmentObject(log)
|
||||||
|
.environmentObject(storage)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showDataTransferView) {
|
.sheet(isPresented: $showDataTransferView) {
|
||||||
if let client = scanner.configuredDevice {
|
if let client = scanner.configuredDevice {
|
||||||
TransferView(
|
TransferView(
|
||||||
bluetoothClient: client)
|
bluetoothClient: client, info: $scanner.lastDeviceInfo)
|
||||||
.environmentObject(storage)
|
.environmentObject(storage)
|
||||||
|
.environmentObject(transfer)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@ -263,7 +271,7 @@ struct ContentView: View {
|
|||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
||||||
ContentView()
|
ContentView(scanner: .init())
|
||||||
.environmentObject(storage)
|
.environmentObject(storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,11 @@ extension UInt16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var low: UInt8 {
|
var low: UInt8 {
|
||||||
UInt8(clamping: self)
|
UInt8(self & 0xFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
var high: UInt8 {
|
var high: UInt8 {
|
||||||
UInt8(clamping: self >> 8)
|
UInt8(self >> 8 & 0xFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
var integer: Int {
|
var integer: Int {
|
||||||
|
@ -15,9 +15,6 @@ final class PersistentStorage: ObservableObject {
|
|||||||
@AppStorage("newestDate")
|
@AppStorage("newestDate")
|
||||||
private var newestMeasurementTime: Int = 0
|
private var newestMeasurementTime: Int = 0
|
||||||
|
|
||||||
@AppStorage("deviceTime")
|
|
||||||
private var lastDeviceTimeData: Data?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The date of the latest measurement.
|
The date of the latest measurement.
|
||||||
|
|
||||||
@ -71,6 +68,8 @@ final class PersistentStorage: ObservableObject {
|
|||||||
|
|
||||||
ensureExistenceOfFolder()
|
ensureExistenceOfFolder()
|
||||||
//recalculateDailyCounts()
|
//recalculateDailyCounts()
|
||||||
|
updateTransferCount()
|
||||||
|
updateDeviceInfoCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ensureExistenceOfFolder() {
|
private func ensureExistenceOfFolder() {
|
||||||
@ -121,7 +120,7 @@ final class PersistentStorage: ObservableObject {
|
|||||||
private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) {
|
private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) {
|
||||||
let startDate = Date().addingTimeInterval(-lastValueInterval).seconds
|
let startDate = Date().addingTimeInterval(-lastValueInterval).seconds
|
||||||
recentMeasurements = (measurements + recentMeasurements)
|
recentMeasurements = (measurements + recentMeasurements)
|
||||||
.filter { $0.id > startDate }
|
.filter { $0.id > startDate }.sorted().reversed()
|
||||||
log.info("\(recentMeasurements.count) recent measurements (with \(measurements.count) new entries)")
|
log.info("\(recentMeasurements.count) recent measurements (with \(measurements.count) new entries)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,35 +277,78 @@ final class PersistentStorage: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Device time
|
// MARK: Device info archive
|
||||||
|
|
||||||
var lastDeviceTime: DeviceTime? {
|
@Published
|
||||||
get {
|
var numberOfStoredDeviceInfos: Int = 0
|
||||||
guard let data = lastDeviceTimeData else {
|
|
||||||
return nil
|
private func updateDeviceInfoCount() {
|
||||||
}
|
let count = countFiles(in: "info")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.numberOfStoredDeviceInfos = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var numberOfStoredTransfers: Int = 0
|
||||||
|
|
||||||
|
private func updateTransferCount() {
|
||||||
|
let count = countFiles(in: "transfers")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.numberOfStoredTransfers = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func countFiles(in folder: String) -> Int {
|
||||||
|
let folder = PersistentStorage.documentDirectory.appendingPathComponent(folder)
|
||||||
|
guard fm.fileExists(atPath: folder.path) else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
return try fm.contentsOfDirectory(atPath: folder.path).count
|
||||||
|
} catch {
|
||||||
|
log.error("Failed to count files in '\(folder)': \(error)")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save(data: Data, date: Date, in folderName: String) -> Bool {
|
||||||
|
let folder = PersistentStorage.documentDirectory.appendingPathComponent(folderName)
|
||||||
|
if !fm.fileExists(atPath: folder.path) {
|
||||||
do {
|
do {
|
||||||
let result: DeviceTime = try BinaryDecoder.decode(from: data)
|
try fm.createDirectory(at: folder, withIntermediateDirectories: false)
|
||||||
return result
|
|
||||||
} catch {
|
} catch {
|
||||||
log.error("Failed to decode device time: \(error)")
|
log.error("Failed to create folder '\(folderName)': \(error)")
|
||||||
lastDeviceTimeData = nil
|
return false
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set {
|
let url = folder.appendingPathComponent("\(date.seconds)")
|
||||||
guard let newValue else {
|
do {
|
||||||
lastDeviceTimeData = nil
|
try data.write(to: url)
|
||||||
return
|
} catch {
|
||||||
}
|
log.error("Failed to write '\(url.lastPathComponent)' in '\(folder)': \(error)")
|
||||||
do {
|
return false
|
||||||
let data = try BinaryEncoder.encode(newValue)
|
|
||||||
lastDeviceTimeData = data
|
|
||||||
} catch {
|
|
||||||
log.error("Failed to encode device time: \(error)")
|
|
||||||
lastDeviceTimeData = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func save(deviceInfo: DeviceInfo) -> Bool {
|
||||||
|
defer { updateDeviceInfoCount() }
|
||||||
|
let data: Data
|
||||||
|
do {
|
||||||
|
data = try BinaryEncoder.encode(deviceInfo)
|
||||||
|
} catch {
|
||||||
|
log.error("Failed to encode device info for storage: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return save(data: data, date: deviceInfo.time.date, in: "info")
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func saveTransferData(data: Data, date: Date) -> Bool {
|
||||||
|
defer { updateTransferCount() }
|
||||||
|
return save(data: data, date: date, in: "transfers")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
private let storage = PersistentStorage()
|
let storage = PersistentStorage()
|
||||||
|
|
||||||
|
private let scanner = BluetoothScanner()
|
||||||
|
|
||||||
|
private let transfer = TransferHandler()
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct TempTrackApp: App {
|
struct TempTrackApp: App {
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView(scanner: scanner)
|
||||||
.environmentObject(storage)
|
.environmentObject(storage)
|
||||||
|
.environmentObject(transfer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,3 +74,7 @@ extension Data {
|
|||||||
return .init(address: address, valueByte: temperatureByte, secondsAgo: time)
|
return .init(address: address, valueByte: temperatureByte, secondsAgo: time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TemperatureSensor: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -45,7 +45,7 @@ struct DeviceInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var nextUpdateText: String {
|
private var nextUpdateText: String {
|
||||||
let secs = info.time.nextMeasurement.secondsToNow
|
let secs = -info.time.nextMeasurement.secondsToNow
|
||||||
guard secs > 1 else {
|
guard secs > 1 else {
|
||||||
return "Now"
|
return "Now"
|
||||||
}
|
}
|
||||||
@ -101,6 +101,9 @@ struct DeviceInfoView: View {
|
|||||||
IconAndTextView(
|
IconAndTextView(
|
||||||
icon: .power,
|
icon: .power,
|
||||||
text: "\(df.string(from: info.time.deviceStartTime)) (\(runTimeString))")
|
text: "\(df.string(from: info.time.deviceStartTime)) (\(runTimeString))")
|
||||||
|
IconAndTextView(
|
||||||
|
icon: .touchid,
|
||||||
|
text: "\(info.uniqueIdOfPowerCycle)")
|
||||||
IconAndTextView(
|
IconAndTextView(
|
||||||
icon: .autostartstop,
|
icon: .autostartstop,
|
||||||
text: "Wakeup: \(info.wakeupReason.text)")
|
text: "Wakeup: \(info.wakeupReason.text)")
|
||||||
@ -124,6 +127,10 @@ struct DeviceInfoView: View {
|
|||||||
IconAndTextView(
|
IconAndTextView(
|
||||||
icon: .iphoneAndArrowForward,
|
icon: .iphoneAndArrowForward,
|
||||||
text: "\(info.transferBlockSize) Byte Block Size")
|
text: "\(info.transferBlockSize) Byte Block Size")
|
||||||
|
IconAndTextView(
|
||||||
|
icon: .externaldriveBadgeCheckmark,
|
||||||
|
text: String(format: "0x%02X 0x%02X",
|
||||||
|
UInt8(info.dataChecksum >> 8), UInt8( info.dataChecksum & 0xFF)))
|
||||||
}
|
}
|
||||||
sensorView(info.sensor0, id: 0)
|
sensorView(info.sensor0, id: 0)
|
||||||
sensorView(info.sensor1, id: 1)
|
sensorView(info.sensor1, id: 1)
|
||||||
|
@ -12,16 +12,27 @@ struct LogView: View {
|
|||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var log: Log
|
var log: Log
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var storage: PersistentStorage
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List(log.logEntries) { entry in
|
List {
|
||||||
VStack(alignment: .leading) {
|
Text("\(storage.numberOfStoredDeviceInfos) device infos")
|
||||||
HStack {
|
.font(.body)
|
||||||
Text(entry.level.description)
|
.foregroundColor(.secondary)
|
||||||
Spacer()
|
Text("\(storage.numberOfStoredTransfers) transfers")
|
||||||
Text(df.string(from: entry.date))
|
.font(.body)
|
||||||
}.font(.footnote)
|
.foregroundColor(.secondary)
|
||||||
Text(entry.message)
|
ForEach(log.logEntries) { entry in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text(entry.level.description)
|
||||||
|
Spacer()
|
||||||
|
Text(df.string(from: entry.date))
|
||||||
|
}.font(.footnote)
|
||||||
|
Text(entry.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Log")
|
.navigationTitle("Log")
|
||||||
|
@ -8,24 +8,17 @@ struct TransferView: View {
|
|||||||
|
|
||||||
let bluetoothClient: BluetoothDevice
|
let bluetoothClient: BluetoothDevice
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var info: DeviceInfo?
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var storage: PersistentStorage
|
var storage: PersistentStorage
|
||||||
|
|
||||||
@State
|
@EnvironmentObject
|
||||||
var bytesTransferred: Double = 0.0
|
var transfer: TransferHandler
|
||||||
|
|
||||||
@State
|
|
||||||
var totalBytes: Double = 0.0
|
|
||||||
|
|
||||||
@State
|
|
||||||
var measurements: [TemperatureMeasurement] = []
|
|
||||||
|
|
||||||
@State
|
|
||||||
var transferIsRunning = false
|
|
||||||
|
|
||||||
|
|
||||||
private var storageIcon: SFSymbol {
|
private var storageIcon: SFSymbol {
|
||||||
guard let info = bluetoothClient.lastDeviceInfo else {
|
guard let info else {
|
||||||
return .externaldrive
|
return .externaldrive
|
||||||
}
|
}
|
||||||
if info.storageSize - info.numberOfRecordedBytes < storageWarnBytes {
|
if info.storageSize - info.numberOfRecordedBytes < storageWarnBytes {
|
||||||
@ -35,14 +28,14 @@ struct TransferView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var measurementsText: String {
|
private var measurementsText: String {
|
||||||
guard let info = bluetoothClient.lastDeviceInfo else {
|
guard let info else {
|
||||||
return "No measurements"
|
return "No measurements"
|
||||||
}
|
}
|
||||||
return "\(info.numberOfStoredMeasurements) measurements (\(info.time.totalNumberOfMeasurements) total)"
|
return "\(info.numberOfStoredMeasurements) measurements (\(info.time.totalNumberOfMeasurements) total)"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var storageText: String {
|
private var storageText: String {
|
||||||
guard let info = bluetoothClient.lastDeviceInfo else {
|
guard let info else {
|
||||||
return "No data"
|
return "No data"
|
||||||
}
|
}
|
||||||
if info.storageSize <= 0 {
|
if info.storageSize <= 0 {
|
||||||
@ -52,25 +45,25 @@ struct TransferView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var transferSizeText: String {
|
private var transferSizeText: String {
|
||||||
guard let info = bluetoothClient.lastDeviceInfo else {
|
guard let info else {
|
||||||
return "No transfer size"
|
return "No transfer size"
|
||||||
}
|
}
|
||||||
return "\(info.transferBlockSize) Byte Block Size"
|
return "\(info.transferBlockSize) Byte Block Size"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var transferByteText: String {
|
private var transferByteText: String {
|
||||||
let total = Int(totalBytes)
|
let total = Int(transfer.totalBytes)
|
||||||
guard total > 0 else {
|
guard total > 0 else {
|
||||||
return "No data"
|
return "No data"
|
||||||
}
|
}
|
||||||
return "\(Int(bytesTransferred)) / \(total) Bytes"
|
return "\(Int(transfer.bytesTransferred)) / \(total) Bytes"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var transferMeasurementText: String {
|
private var transferMeasurementText: String {
|
||||||
guard !measurements.isEmpty else {
|
guard !transfer.measurements.isEmpty else {
|
||||||
return "No measurements"
|
return "No measurements"
|
||||||
}
|
}
|
||||||
return "\(measurements.count) measurements"
|
return "\(transfer.measurements.count) measurements"
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -93,13 +86,13 @@ struct TransferView: View {
|
|||||||
Button(action: clearStorage) {
|
Button(action: clearStorage) {
|
||||||
Text("Remove recorded data")
|
Text("Remove recorded data")
|
||||||
}
|
}
|
||||||
.disabled(transferIsRunning)
|
.disabled(transfer.transferIsRunning)
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("Transfer")
|
Text("Transfer")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
ProgressView(value: bytesTransferred, total: totalBytes)
|
ProgressView(value: transfer.bytesTransferred, total: transfer.totalBytes)
|
||||||
.progressViewStyle(.linear)
|
.progressViewStyle(.linear)
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
IconAndTextView(
|
IconAndTextView(
|
||||||
@ -113,19 +106,19 @@ struct TransferView: View {
|
|||||||
Button(action: transferData) {
|
Button(action: transferData) {
|
||||||
Text("Transfer")
|
Text("Transfer")
|
||||||
}
|
}
|
||||||
.disabled(transferIsRunning)
|
.disabled(transfer.transferIsRunning)
|
||||||
.padding()
|
.padding()
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: saveTransfer) {
|
Button(action: saveTransfer) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
.disabled(transferIsRunning || measurements.isEmpty)
|
.disabled(transfer.transferIsRunning || transfer.measurements.isEmpty)
|
||||||
.padding()
|
.padding()
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: discardTransfer) {
|
Button(action: discardTransfer) {
|
||||||
Text("Discard")
|
Text("Discard")
|
||||||
}
|
}
|
||||||
.disabled(transferIsRunning || measurements.isEmpty)
|
.disabled(transfer.transferIsRunning || transfer.measurements.isEmpty)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -140,80 +133,22 @@ struct TransferView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func transferData() {
|
func transferData() {
|
||||||
guard let info = bluetoothClient.lastDeviceInfo else {
|
guard let info else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
transferIsRunning = true
|
transfer.startTransfer(from: bluetoothClient, with: info, storage: storage)
|
||||||
let total = info.numberOfRecordedBytes
|
|
||||||
let chunkSize = info.transferBlockSize
|
|
||||||
bytesTransferred = 0
|
|
||||||
totalBytes = Double(total)
|
|
||||||
Task {
|
|
||||||
defer {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.transferIsRunning = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var data = Data(capacity: total)
|
|
||||||
while data.count < total {
|
|
||||||
let remainingBytes = total - data.count
|
|
||||||
let currentChunkSize = min(remainingBytes, chunkSize)
|
|
||||||
guard let chunk = await bluetoothClient.getDeviceData(offset: data.count, count: currentChunkSize) else {
|
|
||||||
log.warning("Failed to finish transfer")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.append(chunk)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.bytesTransferred = Double(data.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.bytesTransferred = totalBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
var measurementCount = 0
|
|
||||||
let recordingStart = info.currentMeasurementStartTime
|
|
||||||
while !data.isEmpty {
|
|
||||||
let byte = data.removeFirst()
|
|
||||||
guard (byte == 0xFF) else {
|
|
||||||
log.error("Expected 0xFF at index \(total - data.count - 1)")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
guard data.count >= 2 else {
|
|
||||||
log.error("Expected two more bytes after index \(total - data.count - 1)")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let temp0 = TemperatureValue(byte: data.removeFirst())
|
|
||||||
let temp1 = TemperatureValue(byte: data.removeFirst())
|
|
||||||
let date = recordingStart
|
|
||||||
.addingTimeInterval(TimeInterval(measurementCount * info.measurementInterval))
|
|
||||||
let measurement = TemperatureMeasurement(
|
|
||||||
sensor0: temp0,
|
|
||||||
sensor1: temp1,
|
|
||||||
date: date)
|
|
||||||
measurementCount += 1
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.measurements.append(measurement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func discardTransfer() {
|
func discardTransfer() {
|
||||||
self.measurements = []
|
transfer.discardTransfer()
|
||||||
self.bytesTransferred = 0
|
|
||||||
self.totalBytes = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveTransfer() {
|
func saveTransfer() {
|
||||||
// TODO: Save
|
transfer.saveTransfer(in: storage)
|
||||||
|
|
||||||
discardTransfer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearStorage() {
|
func clearStorage() {
|
||||||
guard let byteCount = bluetoothClient.lastDeviceInfo?.numberOfRecordedBytes else {
|
guard let byteCount = info?.numberOfRecordedBytes else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Task {
|
Task {
|
||||||
@ -229,7 +164,7 @@ struct TransferView: View {
|
|||||||
struct TransferView_Previews: PreviewProvider {
|
struct TransferView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
||||||
TransferView(bluetoothClient: .init())
|
TransferView(bluetoothClient: .init(), info: .constant(.mock))
|
||||||
.environmentObject(storage)
|
.environmentObject(storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user