Transfer view, change data flow, actors
This commit is contained in:
parent
8b4c4800c9
commit
396571fd30
@ -28,7 +28,7 @@
|
|||||||
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0602A25108100114294 /* BluetoothClient.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 /* TemperatureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0672A2698B400114294 /* TemperatureStorage.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 */; };
|
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */; };
|
||||||
@ -42,6 +42,18 @@
|
|||||||
E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FA2A39C82D005204C3 /* LogView.swift */; };
|
E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FA2A39C82D005204C3 /* LogView.swift */; };
|
||||||
E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FC2A39C86B005204C3 /* LogEntry.swift */; };
|
E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FC2A39C86B005204C3 /* LogEntry.swift */; };
|
||||||
E2A553FF2A3A1024005204C3 /* DayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FE2A3A1024005204C3 /* DayView.swift */; };
|
E2A553FF2A3A1024005204C3 /* DayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FE2A3A1024005204C3 /* DayView.swift */; };
|
||||||
|
E2A554012A3A6403005204C3 /* DeviceTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554002A3A6403005204C3 /* DeviceTime.swift */; };
|
||||||
|
E2A554052A4ADA93005204C3 /* TransferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554042A4ADA93005204C3 /* TransferView.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 */; };
|
||||||
|
E2A5540E2A4C9C4C005204C3 /* BluetoothRequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */; };
|
||||||
|
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */; };
|
||||||
|
E2A554142A4C9C96005204C3 /* DeviceDataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554132A4C9C96005204C3 /* DeviceDataRequest.swift */; };
|
||||||
|
E2A554162A4C9D2E005204C3 /* DeviceDataResetRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A554152A4C9D2E005204C3 /* DeviceDataResetRequest.swift */; };
|
||||||
|
E2E69B602A4CD48F00C6035E /* IconAndTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */; };
|
||||||
|
E2E69B622A4D7C3100C6035E /* BluetoothScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */; };
|
||||||
|
E2E69B662A4DA48B00C6035E /* BluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -65,7 +77,7 @@
|
|||||||
88CDE05E2A250F5200114294 /* DeviceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceState.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>"; };
|
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 /* TemperatureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureStorage.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>"; };
|
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagerDelegate.swift; sourceTree = "<group>"; };
|
||||||
@ -79,6 +91,18 @@
|
|||||||
E2A553FA2A39C82D005204C3 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
|
E2A553FA2A39C82D005204C3 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
|
||||||
E2A553FC2A39C86B005204C3 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
|
E2A553FC2A39C86B005204C3 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
|
||||||
E2A553FE2A3A1024005204C3 /* DayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayView.swift; sourceTree = "<group>"; };
|
E2A553FE2A3A1024005204C3 /* DayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayView.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>"; };
|
||||||
|
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>"; };
|
||||||
|
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>"; };
|
||||||
|
E2A554132A4C9C96005204C3 /* DeviceDataRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDataRequest.swift; sourceTree = "<group>"; };
|
||||||
|
E2A554152A4C9D2E005204C3 /* DeviceDataResetRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDataResetRequest.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>"; };
|
||||||
|
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevice.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -98,7 +122,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */,
|
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */,
|
||||||
88CDE0672A2698B400114294 /* TemperatureStorage.swift */,
|
88CDE0672A2698B400114294 /* PersistentStorage.swift */,
|
||||||
E2A553F82A399F58005204C3 /* Log.swift */,
|
E2A553F82A399F58005204C3 /* Log.swift */,
|
||||||
E2A553FC2A39C86B005204C3 /* LogEntry.swift */,
|
E2A553FC2A39C86B005204C3 /* LogEntry.swift */,
|
||||||
);
|
);
|
||||||
@ -124,6 +148,7 @@
|
|||||||
88CDE04D2A2508E900114294 /* TempTrack */ = {
|
88CDE04D2A2508E900114294 /* TempTrack */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2A5540A2A4ADD1D005204C3 /* Connection */,
|
||||||
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
||||||
88CDE0502A2508E900114294 /* ContentView.swift */,
|
88CDE0502A2508E900114294 /* ContentView.swift */,
|
||||||
E253A9202A2B39A700EC6B28 /* Extensions */,
|
E253A9202A2B39A700EC6B28 /* Extensions */,
|
||||||
@ -152,6 +177,7 @@
|
|||||||
88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */,
|
88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */,
|
||||||
88CDE0752A28AF0900114294 /* TemperatureValue.swift */,
|
88CDE0752A28AF0900114294 /* TemperatureValue.swift */,
|
||||||
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */,
|
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */,
|
||||||
|
E2A554062A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift */,
|
||||||
);
|
);
|
||||||
path = Temperature;
|
path = Temperature;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -166,6 +192,7 @@
|
|||||||
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
||||||
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
||||||
88CDE06C2A28A92000114294 /* DeviceInfo.swift */,
|
88CDE06C2A28A92000114294 /* DeviceInfo.swift */,
|
||||||
|
E2A554002A3A6403005204C3 /* DeviceTime.swift */,
|
||||||
88404DEA2A37BE3000D30244 /* DeviceWakeCause.swift */,
|
88404DEA2A37BE3000D30244 /* DeviceWakeCause.swift */,
|
||||||
);
|
);
|
||||||
path = Bluetooth;
|
path = Bluetooth;
|
||||||
@ -181,6 +208,8 @@
|
|||||||
88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */,
|
88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */,
|
||||||
E2A553FA2A39C82D005204C3 /* LogView.swift */,
|
E2A553FA2A39C82D005204C3 /* LogView.swift */,
|
||||||
E2A553FE2A3A1024005204C3 /* DayView.swift */,
|
E2A553FE2A3A1024005204C3 /* DayView.swift */,
|
||||||
|
E2A554042A4ADA93005204C3 /* TransferView.swift */,
|
||||||
|
E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -198,6 +227,21 @@
|
|||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E2A5540A2A4ADD1D005204C3 /* Connection */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E2A554082A4ADCC9005204C3 /* DeviceConnection.swift */,
|
||||||
|
E2A5540D2A4C9C4C005204C3 /* BluetoothRequestType.swift */,
|
||||||
|
E2A5540F2A4C9C68005204C3 /* DeviceRequest.swift */,
|
||||||
|
E2A5540B2A4ADFC6005204C3 /* DeviceInfoRequest.swift */,
|
||||||
|
E2A554132A4C9C96005204C3 /* DeviceDataRequest.swift */,
|
||||||
|
E2A554152A4C9D2E005204C3 /* DeviceDataResetRequest.swift */,
|
||||||
|
E2E69B612A4D7C3100C6035E /* BluetoothScanner.swift */,
|
||||||
|
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */,
|
||||||
|
);
|
||||||
|
path = Connection;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -277,28 +321,40 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
88404DDB2A2F4DCA00D30244 /* MeasurementDailyCount.swift in Sources */,
|
88404DDB2A2F4DCA00D30244 /* MeasurementDailyCount.swift in Sources */,
|
||||||
|
E2A554142A4C9C96005204C3 /* DeviceDataRequest.swift in Sources */,
|
||||||
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
||||||
88CDE05F2A250F5200114294 /* DeviceState.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 */,
|
||||||
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */,
|
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */,
|
||||||
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */,
|
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */,
|
||||||
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
||||||
|
E2E69B602A4CD48F00C6035E /* IconAndTextView.swift in Sources */,
|
||||||
|
E2A554052A4ADA93005204C3 /* TransferView.swift in Sources */,
|
||||||
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */,
|
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */,
|
||||||
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */,
|
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */,
|
||||||
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */,
|
E2A554012A3A6403005204C3 /* DeviceTime.swift in Sources */,
|
||||||
|
88CDE0682A2698B400114294 /* PersistentStorage.swift in Sources */,
|
||||||
88404DE92A31F7D500D30244 /* Data+Extensions.swift in Sources */,
|
88404DE92A31F7D500D30244 /* Data+Extensions.swift in Sources */,
|
||||||
88404DE52A31F23E00D30244 /* UInt16+Extensions.swift in Sources */,
|
88404DE52A31F23E00D30244 /* UInt16+Extensions.swift in Sources */,
|
||||||
88404DD42A2F0DB100D30244 /* Date+Extensions.swift in Sources */,
|
88404DD42A2F0DB100D30244 /* Date+Extensions.swift in Sources */,
|
||||||
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
||||||
|
E2E69B622A4D7C3100C6035E /* BluetoothScanner.swift in Sources */,
|
||||||
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */,
|
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */,
|
||||||
|
E2A5540C2A4ADFC6005204C3 /* DeviceInfoRequest.swift in Sources */,
|
||||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
88CDE07B2A28AF5100114294 /* BluetoothRequest.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 */,
|
||||||
|
E2A5540E2A4C9C4C005204C3 /* BluetoothRequestType.swift in Sources */,
|
||||||
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */,
|
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */,
|
||||||
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
||||||
|
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */,
|
||||||
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
||||||
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */,
|
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */,
|
||||||
|
E2A554072A4ADB9C005204C3 /* TemperatureDataTransferDelegate.swift in Sources */,
|
||||||
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */,
|
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */,
|
||||||
88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */,
|
88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */,
|
||||||
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
||||||
|
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
/*
|
||||||
final class BluetoothClient: ObservableObject {
|
final class BluetoothClient: ObservableObject {
|
||||||
|
|
||||||
private let updateInterval = 3.0
|
private let updateInterval = 3.0
|
||||||
@ -9,7 +9,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
|
|
||||||
private let connection = DeviceManager()
|
private let connection = DeviceManager()
|
||||||
|
|
||||||
private let storage: TemperatureStorage
|
private let storage: PersistentStorage
|
||||||
|
|
||||||
var hasInfo: Bool {
|
var hasInfo: Bool {
|
||||||
deviceInfo != nil
|
deviceInfo != nil
|
||||||
@ -22,9 +22,23 @@ final class BluetoothClient: ObservableObject {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
init(storage: TemperatureStorage, deviceInfo: DeviceInfo? = nil) {
|
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.storage = storage
|
||||||
self.deviceInfo = deviceInfo
|
self.deviceInfo = deviceInfo
|
||||||
|
self.shouldConnect = shouldConnect
|
||||||
|
connection.shouldConnectIfPossible = shouldConnect
|
||||||
connection.delegate = self
|
connection.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +61,6 @@ final class BluetoothClient: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
private(set) var deviceInfo: DeviceInfo? {
|
private(set) var deviceInfo: DeviceInfo? {
|
||||||
didSet {
|
didSet {
|
||||||
updateDeviceTimeIfNeeded()
|
|
||||||
// collectRecordedData()
|
// collectRecordedData()
|
||||||
if let deviceInfo, let runningTransfer {
|
if let deviceInfo, let runningTransfer {
|
||||||
runningTransfer.update(info: deviceInfo)
|
runningTransfer.update(info: deviceInfo)
|
||||||
@ -103,6 +116,14 @@ final class BluetoothClient: ObservableObject {
|
|||||||
|
|
||||||
// MARK: Requests
|
// 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() {
|
private func performNextRequest() {
|
||||||
guard runningRequest == nil else {
|
guard runningRequest == nil else {
|
||||||
return
|
return
|
||||||
@ -136,24 +157,6 @@ final class BluetoothClient: ObservableObject {
|
|||||||
openRequests.append(request)
|
openRequests.append(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Device time
|
|
||||||
|
|
||||||
private func updateDeviceTimeIfNeeded() {
|
|
||||||
guard let deviceInfo else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard !deviceInfo.hasDeviceStartTimeSet || abs(deviceInfo.clockOffset) > minimumOffsetToUpdateDeviceClock else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !openRequests.contains(where: { if case .setDeviceStartTime = $0 { return true }; return false }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let time = deviceInfo.calculatedDeviceStartTime.seconds
|
|
||||||
addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time))
|
|
||||||
log.info("Setting device start time to \(time) s (correcting offset of \(Int(deviceInfo.clockOffset)) s)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Data transfer
|
// MARK: Data transfer
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
@ -173,8 +176,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
guard info.numberOfStoredMeasurements > 0 else {
|
guard info.numberOfStoredMeasurements > 0 else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
let transfer = TemperatureDataTransfer(info: info, previous: storage.lastDeviceTime)
|
||||||
let transfer = TemperatureDataTransfer(info: info)
|
|
||||||
runningTransfer = transfer
|
runningTransfer = transfer
|
||||||
let next = transfer.nextRequest()
|
let next = transfer.nextRequest()
|
||||||
log.info("Starting transfer")
|
log.info("Starting transfer")
|
||||||
@ -185,9 +187,13 @@ final class BluetoothClient: ObservableObject {
|
|||||||
private func didReceive(data: Data, offset: Int, count: Int) {
|
private func didReceive(data: Data, offset: Int, count: Int) {
|
||||||
guard let runningTransfer else {
|
guard let runningTransfer else {
|
||||||
log.warning("No running transfer to process device data")
|
log.warning("No running transfer to process device data")
|
||||||
|
self.runningRequest = nil
|
||||||
return // TODO: Start new transfer?
|
return // TODO: Start new transfer?
|
||||||
}
|
}
|
||||||
runningTransfer.add(data: data, offset: offset, count: count)
|
guard runningTransfer.add(data: data, offset: offset, count: count) else {
|
||||||
|
self.runningRequest = nil
|
||||||
|
return // TODO: Start new transfer
|
||||||
|
}
|
||||||
let next = runningTransfer.nextRequest()
|
let next = runningTransfer.nextRequest()
|
||||||
addRequest(next)
|
addRequest(next)
|
||||||
}
|
}
|
||||||
@ -203,6 +209,13 @@ final class BluetoothClient: ObservableObject {
|
|||||||
|
|
||||||
extension BluetoothClient: DeviceManagerDelegate {
|
extension BluetoothClient: DeviceManagerDelegate {
|
||||||
|
|
||||||
|
func deviceManager(shouldConnectToDevice: Bool) {
|
||||||
|
guard !isUpdatingFlag else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.shouldConnect = shouldConnectToDevice
|
||||||
|
}
|
||||||
|
|
||||||
func deviceManager(didReceive data: Data) {
|
func deviceManager(didReceive data: Data) {
|
||||||
defer {
|
defer {
|
||||||
performNextRequest()
|
performNextRequest()
|
||||||
@ -232,20 +245,21 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
return
|
return
|
||||||
case .invalidNumberOfBytesToDelete:
|
case .invalidNumberOfBytesToDelete:
|
||||||
guard case .clearRecordingBuffer = runningRequest else {
|
guard case .clearRecordingBuffer = runningRequest else {
|
||||||
// If clearing the recording buffer fails due to byte mismatch,
|
log.error("Request \(runningRequest) received non-matching response about number of bytes to delete")
|
||||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
|
||||||
addRequest(.getInfo)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.error("Request \(runningRequest) received non-matching responde about number of bytes to delete")
|
// 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:
|
case .responseTooLarge:
|
||||||
guard case .getRecordingData = runningRequest else {
|
guard case .getRecordingData = runningRequest else {
|
||||||
// If requesting bytes fails due to the response size,
|
log.error("Unexpectedly exceeded payload size for request \(runningRequest)")
|
||||||
// then requesting new info will update the response size, and the transfer will be resumed
|
|
||||||
addRequest(.getInfo)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.error("Unexpectedly exceeded payload size for request \(runningRequest)")
|
// 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:
|
default:
|
||||||
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
||||||
// If clearing the recording buffer fails due to byte mismatch,
|
// If clearing the recording buffer fails due to byte mismatch,
|
||||||
@ -264,10 +278,6 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
didReceive(data: payload, offset: offset, count: count)
|
didReceive(data: payload, offset: offset, count: count)
|
||||||
case .clearRecordingBuffer:
|
case .clearRecordingBuffer:
|
||||||
didClearDeviceStorage()
|
didClearDeviceStorage()
|
||||||
|
|
||||||
case .setDeviceStartTime:
|
|
||||||
log.info("Device time set")
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,11 +286,12 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
log.warning("No running transfer after clearing device storage")
|
log.warning("No running transfer after clearing device storage")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runningTransfer.completeTransfer()
|
defer { self.runningTransfer = nil }
|
||||||
|
guard runningTransfer.completeTransfer() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
storage.add(runningTransfer.measurements)
|
storage.add(runningTransfer.measurements)
|
||||||
self.runningTransfer = nil
|
storage.lastDeviceTime = runningTransfer.time
|
||||||
|
|
||||||
updateDeviceTimeIfNeeded()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceManager(didChangeState state: DeviceState) {
|
func deviceManager(didChangeState state: DeviceState) {
|
||||||
@ -290,3 +301,4 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
/*
|
||||||
enum BluetoothRequest {
|
enum BluetoothRequest {
|
||||||
/**
|
/**
|
||||||
* Request the number of bytes already recorded
|
* Request the number of bytes already recorded
|
||||||
@ -44,11 +44,6 @@ enum BluetoothRequest {
|
|||||||
*/
|
*/
|
||||||
case clearRecordingBuffer(byteCount: Int)
|
case clearRecordingBuffer(byteCount: Int)
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
*/
|
|
||||||
case setDeviceStartTime(deviceStartTimeSeconds: Int)
|
|
||||||
|
|
||||||
var serialized: Data {
|
var serialized: Data {
|
||||||
let firstByte = Data([byte])
|
let firstByte = Data([byte])
|
||||||
switch self {
|
switch self {
|
||||||
@ -58,8 +53,6 @@ enum BluetoothRequest {
|
|||||||
return firstByte + count.twoByteData + offset.twoByteData
|
return firstByte + count.twoByteData + offset.twoByteData
|
||||||
case .clearRecordingBuffer(let byteCount):
|
case .clearRecordingBuffer(let byteCount):
|
||||||
return firstByte + byteCount.twoByteData
|
return firstByte + byteCount.twoByteData
|
||||||
case .setDeviceStartTime(let deviceStartTimeSeconds):
|
|
||||||
return firstByte + deviceStartTimeSeconds.fourByteData
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +61,7 @@ enum BluetoothRequest {
|
|||||||
case .getInfo: return 0
|
case .getInfo: return 0
|
||||||
case .getRecordingData: return 1
|
case .getRecordingData: return 1
|
||||||
case .clearRecordingBuffer: return 2
|
case .clearRecordingBuffer: return 2
|
||||||
case .setDeviceStartTime: return 3
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
@ -2,7 +2,11 @@ import Foundation
|
|||||||
|
|
||||||
struct DeviceInfo {
|
struct DeviceInfo {
|
||||||
|
|
||||||
let receivedDate: Date
|
/**
|
||||||
|
The maximum factor by which the device clock can run
|
||||||
|
*/
|
||||||
|
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
|
||||||
@ -10,31 +14,19 @@ struct DeviceInfo {
|
|||||||
/// The number of measurements already performed
|
/// The number of measurements already performed
|
||||||
let numberOfStoredMeasurements: Int
|
let numberOfStoredMeasurements: Int
|
||||||
|
|
||||||
/// The measurements since device start
|
|
||||||
let totalNumberOfMeasurements: Int
|
|
||||||
|
|
||||||
/// The interval between measurements (in seconds)
|
/// The interval between measurements (in seconds)
|
||||||
let measurementInterval: Int
|
let measurementInterval: Int
|
||||||
|
|
||||||
let nextMeasurement: Date
|
|
||||||
|
|
||||||
let sensor0: TemperatureSensor?
|
let sensor0: TemperatureSensor?
|
||||||
|
|
||||||
let sensor1: TemperatureSensor?
|
let sensor1: TemperatureSensor?
|
||||||
|
|
||||||
// MARK: Device time
|
// MARK: Device time
|
||||||
|
|
||||||
/**
|
|
||||||
The number of seconds the device has been powered on
|
|
||||||
*/
|
|
||||||
let numberOfSecondsRunning: Int
|
|
||||||
|
|
||||||
let deviceStartTime: Date
|
|
||||||
|
|
||||||
let hasDeviceStartTimeSet: Bool
|
|
||||||
|
|
||||||
let wakeupReason: DeviceWakeCause
|
let wakeupReason: DeviceWakeCause
|
||||||
|
|
||||||
|
let time: DeviceTime
|
||||||
|
|
||||||
// MARK: Storage
|
// MARK: Storage
|
||||||
|
|
||||||
let storageSize: Int
|
let storageSize: Int
|
||||||
@ -50,47 +42,76 @@ struct DeviceInfo {
|
|||||||
Int((storageFillRatio * 100).rounded())
|
Int((storageFillRatio * 100).rounded())
|
||||||
}
|
}
|
||||||
|
|
||||||
var clockOffset: TimeInterval {
|
var currentMeasurementStartTime: Date {
|
||||||
// Measurements are performed on device start (-1) and also count next measurement (+1)
|
time.nextMeasurement.addingTimeInterval(-Double(numberOfStoredMeasurements * measurementInterval))
|
||||||
let nextMeasurementTime = deviceStartTime.adding(seconds: totalNumberOfMeasurements * measurementInterval)
|
|
||||||
return nextMeasurement.timeIntervalSince(nextMeasurementTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var calculatedDeviceStartTime: Date {
|
func estimatedTimeDilation(to previous: DeviceTime?) -> (start: Date, dilation: Double) {
|
||||||
let runtime = totalNumberOfMeasurements * measurementInterval
|
let trivialResult = (start: currentMeasurementStartTime, dilation: 1.0)
|
||||||
return nextMeasurement.adding(seconds: -runtime)
|
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()
|
||||||
|
|
||||||
var data = info
|
var data = info
|
||||||
|
|
||||||
let date = Date().nearestSecond
|
|
||||||
self.receivedDate = date
|
|
||||||
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
self.numberOfRecordedBytes = try data.decodeTwoByteInteger()
|
||||||
self.nextMeasurement = date.adding(seconds: try data.decodeTwoByteInteger())
|
let secondsUntilNextMeasurement = try data.decodeTwoByteInteger()
|
||||||
self.measurementInterval = try data.decodeTwoByteInteger()
|
self.measurementInterval = try data.decodeTwoByteInteger()
|
||||||
self.numberOfStoredMeasurements = try data.decodeTwoByteInteger()
|
self.numberOfStoredMeasurements = try data.decodeTwoByteInteger()
|
||||||
self.totalNumberOfMeasurements = try data.decodeFourByteInteger()
|
let totalNumberOfMeasurements = try data.decodeFourByteInteger()
|
||||||
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()
|
||||||
self.numberOfSecondsRunning = secondsSincePowerOn
|
|
||||||
let deviceStartTimeSeconds = try data.decodeFourByteInteger()
|
self.time = .init(
|
||||||
|
date: date,
|
||||||
|
secondsSincePowerOn: secondsSincePowerOn,
|
||||||
|
totalNumberOfMeasurements: totalNumberOfMeasurements,
|
||||||
|
secondsUntilNextMeasurement: secondsUntilNextMeasurement)
|
||||||
|
let _ = try data.decodeFourByteInteger()
|
||||||
self.sensor0 = try data.decodeSensor()
|
self.sensor0 = try data.decodeSensor()
|
||||||
self.sensor1 = try data.decodeSensor()
|
self.sensor1 = try data.decodeSensor()
|
||||||
self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED
|
self.wakeupReason = .init(rawValue: try data.getByte()) ?? .WAKEUP_UNDEFINED
|
||||||
|
|
||||||
if deviceStartTimeSeconds != 0 {
|
|
||||||
self.hasDeviceStartTimeSet = true
|
|
||||||
self.deviceStartTime = Date(seconds: deviceStartTimeSeconds)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
self.hasDeviceStartTimeSet = false
|
|
||||||
self.deviceStartTime = Date(seconds: date.seconds - secondsSincePowerOn) // Round to nearest second
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,18 +119,13 @@ extension DeviceInfo {
|
|||||||
|
|
||||||
static var mock: DeviceInfo {
|
static var mock: DeviceInfo {
|
||||||
.init(
|
.init(
|
||||||
receivedDate: Date(),
|
|
||||||
numberOfRecordedBytes: 123,
|
numberOfRecordedBytes: 123,
|
||||||
numberOfStoredMeasurements: 234,
|
numberOfStoredMeasurements: 234,
|
||||||
totalNumberOfMeasurements: 345,
|
|
||||||
measurementInterval: 60,
|
measurementInterval: 60,
|
||||||
nextMeasurement: .now.addingTimeInterval(5),
|
|
||||||
sensor0: .init(address: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], value: .value(21.0), date: .now.addingTimeInterval(-2)),
|
sensor0: .init(address: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], value: .value(21.0), date: .now.addingTimeInterval(-2)),
|
||||||
sensor1: .init(address: [0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09], value: .value(19.0), date: .now.addingTimeInterval(-4)),
|
sensor1: .init(address: [0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09], value: .value(19.0), date: .now.addingTimeInterval(-4)),
|
||||||
numberOfSecondsRunning: 20,
|
|
||||||
deviceStartTime: .now.addingTimeInterval(-20755),
|
|
||||||
hasDeviceStartTimeSet: true,
|
|
||||||
wakeupReason: .WAKEUP_EXT0,
|
wakeupReason: .WAKEUP_EXT0,
|
||||||
|
time: .mock,
|
||||||
storageSize: 10000,
|
storageSize: 10000,
|
||||||
transferBlockSize: 180)
|
transferBlockSize: 180)
|
||||||
}
|
}
|
||||||
|
@ -24,16 +24,13 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
self.manager = CBCentralManager(delegate: self, queue: nil)
|
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private var dataUpdateTimer: Timer?
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func connect() -> Bool {
|
func connect() -> Bool {
|
||||||
switch state {
|
switch state {
|
||||||
case .bluetoothDisabled:
|
case .bluetoothDisabled:
|
||||||
log.info("Can't connect, bluetooth disabled")
|
log.info("Can't connect, bluetooth disabled")
|
||||||
return false
|
return false
|
||||||
case .disconnected, .bluetoothEnabled:
|
case .disconnected:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
@ -42,18 +39,53 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
state = .scanning
|
state = .scanning
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
shouldConnectIfPossible = true
|
if !shouldConnectIfPossible {
|
||||||
|
shouldConnectIfPossible = true
|
||||||
|
}
|
||||||
state = .scanning
|
state = .scanning
|
||||||
manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID])
|
manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID])
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private var shouldConnectIfPossible = 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() {
|
func disconnect() {
|
||||||
shouldConnectIfPossible = false
|
if shouldConnectIfPossible {
|
||||||
|
shouldConnectIfPossible = false
|
||||||
|
}
|
||||||
switch state {
|
switch state {
|
||||||
case .bluetoothDisabled, .bluetoothEnabled:
|
case .bluetoothDisabled, .disconnected:
|
||||||
return
|
return
|
||||||
case .scanning:
|
case .scanning:
|
||||||
manager.stopScan()
|
manager.stopScan()
|
||||||
@ -67,8 +99,6 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
manager.stopScan()
|
manager.stopScan()
|
||||||
state = .disconnected
|
state = .disconnected
|
||||||
return
|
return
|
||||||
case .disconnected:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +121,9 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
guard shouldConnectIfPossible else {
|
||||||
|
return
|
||||||
|
}
|
||||||
peripheral.delegate = self
|
peripheral.delegate = self
|
||||||
manager.connect(peripheral)
|
manager.connect(peripheral)
|
||||||
manager.stopScan()
|
manager.stopScan()
|
||||||
@ -102,7 +135,7 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
case .poweredOff:
|
case .poweredOff:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
case .poweredOn:
|
case .poweredOn:
|
||||||
state = .bluetoothEnabled
|
state = .disconnected
|
||||||
connect()
|
connect()
|
||||||
case .unsupported:
|
case .unsupported:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
|
@ -2,6 +2,8 @@ import Foundation
|
|||||||
|
|
||||||
protocol DeviceManagerDelegate: AnyObject {
|
protocol DeviceManagerDelegate: AnyObject {
|
||||||
|
|
||||||
|
func deviceManager(shouldConnectToDevice: Bool)
|
||||||
|
|
||||||
func deviceManager(didReceive data: Data)
|
func deviceManager(didReceive data: Data)
|
||||||
|
|
||||||
func deviceManager(didChangeState state: DeviceState)
|
func deviceManager(didChangeState state: DeviceState)
|
||||||
|
@ -5,8 +5,6 @@ enum DeviceState {
|
|||||||
|
|
||||||
case bluetoothDisabled
|
case bluetoothDisabled
|
||||||
|
|
||||||
case bluetoothEnabled
|
|
||||||
|
|
||||||
case scanning
|
case scanning
|
||||||
|
|
||||||
case connecting(device: CBPeripheral)
|
case connecting(device: CBPeripheral)
|
||||||
@ -23,8 +21,6 @@ enum DeviceState {
|
|||||||
switch self {
|
switch self {
|
||||||
case .bluetoothDisabled:
|
case .bluetoothDisabled:
|
||||||
return "Bluetooth is disabled"
|
return "Bluetooth is disabled"
|
||||||
case .bluetoothEnabled:
|
|
||||||
return "Bluetooth enabled"
|
|
||||||
case .scanning:
|
case .scanning:
|
||||||
return "Scanning..."
|
return "Scanning..."
|
||||||
case .connecting(let device):
|
case .connecting(let device):
|
||||||
@ -45,6 +41,18 @@ enum DeviceState {
|
|||||||
return "Not connected"
|
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 {
|
extension DeviceState: CustomStringConvertible {
|
||||||
@ -53,8 +61,6 @@ extension DeviceState: CustomStringConvertible {
|
|||||||
switch self {
|
switch self {
|
||||||
case .bluetoothDisabled:
|
case .bluetoothDisabled:
|
||||||
return "Bluetooth disabled"
|
return "Bluetooth disabled"
|
||||||
case .bluetoothEnabled:
|
|
||||||
return "Bluetooth enabled"
|
|
||||||
case .scanning:
|
case .scanning:
|
||||||
return "Searching for device"
|
return "Searching for device"
|
||||||
case .connecting:
|
case .connecting:
|
||||||
|
70
TempTrack/Bluetooth/DeviceTime.swift
Normal file
70
TempTrack/Bluetooth/DeviceTime.swift
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DeviceTime {
|
||||||
|
|
||||||
|
let date: Date
|
||||||
|
|
||||||
|
let secondsSincePowerOn: Int
|
||||||
|
|
||||||
|
let totalNumberOfMeasurements: Int
|
||||||
|
|
||||||
|
let secondsUntilNextMeasurement: Int
|
||||||
|
|
||||||
|
var nextMeasurement: Date {
|
||||||
|
date.adding(seconds: secondsUntilNextMeasurement)
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceStartTime: Date {
|
||||||
|
date.adding(seconds: -secondsSincePowerOn)
|
||||||
|
}
|
||||||
|
|
||||||
|
var estimatedMeasurementInterval: TimeInterval {
|
||||||
|
guard totalNumberOfMeasurements > 0 else {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
return Double(secondsSincePowerOn + secondsUntilNextMeasurement) / Double(totalNumberOfMeasurements)
|
||||||
|
}
|
||||||
|
|
||||||
|
func measurementStartTime(measurementInterval interval: TimeInterval) -> Date {
|
||||||
|
nextMeasurement.addingTimeInterval(-Double(totalNumberOfMeasurements) * interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func measurementOffset(measurementInterval interval: TimeInterval) -> TimeInterval {
|
||||||
|
measurementStartTime(measurementInterval: interval).timeIntervalSince(deviceStartTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceTime: Equatable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceTime: Codable {
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
var container = try decoder.unkeyedContainer()
|
||||||
|
let time = try container.decode(Double.self)
|
||||||
|
self.date = .init(timeIntervalSince1970: time)
|
||||||
|
self.secondsSincePowerOn = try container.decode(Int.self)
|
||||||
|
self.totalNumberOfMeasurements = try container.decode(Int.self)
|
||||||
|
self.secondsUntilNextMeasurement = try container.decode(Int.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.unkeyedContainer()
|
||||||
|
try container.encode(date.timeIntervalSince1970)
|
||||||
|
try container.encode(secondsSincePowerOn)
|
||||||
|
try container.encode(totalNumberOfMeasurements)
|
||||||
|
try container.encode(secondsUntilNextMeasurement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceTime {
|
||||||
|
|
||||||
|
static var mock: DeviceTime {
|
||||||
|
.init(
|
||||||
|
date: .now,
|
||||||
|
secondsSincePowerOn: 125,
|
||||||
|
totalNumberOfMeasurements: 3,
|
||||||
|
secondsUntilNextMeasurement: 55)
|
||||||
|
}
|
||||||
|
}
|
150
TempTrack/Connection/BluetoothDevice.swift
Normal file
150
TempTrack/Connection/BluetoothDevice.swift
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import Foundation
|
||||||
|
import CoreBluetooth
|
||||||
|
|
||||||
|
actor BluetoothDevice: NSObject, ObservableObject {
|
||||||
|
|
||||||
|
private let peripheral: CBPeripheral!
|
||||||
|
|
||||||
|
private let characteristic: CBCharacteristic!
|
||||||
|
|
||||||
|
@MainActor @Published
|
||||||
|
var lastDeviceInfo: DeviceInfo?
|
||||||
|
|
||||||
|
@Published
|
||||||
|
private(set) var lastRSSI: Int = 0
|
||||||
|
|
||||||
|
init(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
|
||||||
|
self.peripheral = peripheral
|
||||||
|
self.characteristic = characteristic
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
peripheral.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.peripheral = nil
|
||||||
|
self.characteristic = nil
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var requestContinuation: (id: Int, call: CheckedContinuation<Data?, Never>)?
|
||||||
|
|
||||||
|
func updateInfo() async {
|
||||||
|
guard let info = await getInfo() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task { @MainActor in
|
||||||
|
lastDeviceInfo = info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestData = Data([request.type.rawValue]) + request.payload
|
||||||
|
let responseData: Data? = await withCheckedContinuation { continuation in
|
||||||
|
let id = Int.random(in: .min...Int.max)
|
||||||
|
requestContinuation = (id, continuation)
|
||||||
|
peripheral.writeValue(requestData, for: characteristic, type: .withResponse)
|
||||||
|
peripheral.readValue(for: characteristic)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
|
||||||
|
Task {
|
||||||
|
await self?.checkTimeoutForCurrentRequest(request.type, id: id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, id: Int) {
|
||||||
|
guard let requestContinuation else { return }
|
||||||
|
guard requestContinuation.id == id else { return }
|
||||||
|
log.info("Timed out for request \(type)")
|
||||||
|
requestContinuation.call.resume(returning: nil)
|
||||||
|
self.requestContinuation = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BluetoothDevice: CBPeripheralDelegate {
|
||||||
|
|
||||||
|
nonisolated
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 characteristic.uuid == self.characteristic.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.call.resume(returning: response)
|
||||||
|
self.requestContinuation = nil
|
||||||
|
}
|
||||||
|
}
|
176
TempTrack/Connection/BluetoothScanner.swift
Normal file
176
TempTrack/Connection/BluetoothScanner.swift
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import CoreBluetooth
|
||||||
|
|
||||||
|
final class BluetoothScanner: NSObject, CBCentralManagerDelegate, ObservableObject {
|
||||||
|
|
||||||
|
enum ConnectionState {
|
||||||
|
case noDeviceFound
|
||||||
|
case connecting
|
||||||
|
case discoveringService
|
||||||
|
case discoveringCharacteristic
|
||||||
|
}
|
||||||
|
|
||||||
|
private let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
|
||||||
|
|
||||||
|
private let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")
|
||||||
|
|
||||||
|
private var manager: CBCentralManager! = nil
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var bluetoothIsAvailable = false
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var connectionState: ConnectionState
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var configuredDevice: BluetoothDevice?
|
||||||
|
|
||||||
|
private var connectingDevice: CBPeripheral?
|
||||||
|
|
||||||
|
var isScanningForDevices: Bool {
|
||||||
|
get {
|
||||||
|
manager.isScanning
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if newValue {
|
||||||
|
guard !manager.isScanning else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.scanForPeripherals(withServices: [serviceUUID])
|
||||||
|
log.info("Scanner: Started scanning for devices")
|
||||||
|
} else {
|
||||||
|
guard manager.isScanning else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.stopScan()
|
||||||
|
log.info("Scanner: Stopped scanning for devices")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
super.init()
|
||||||
|
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||||
|
guard connectionState == .noDeviceFound && configuredDevice == nil && connectingDevice == nil else {
|
||||||
|
log.info("Scanner: Discovered additional device '\(peripheral.name ?? "No Name")'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.info("Scanner: Connecting to discovered device '\(peripheral.name ?? "No Name")'")
|
||||||
|
connectingDevice = peripheral
|
||||||
|
manager.connect(peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
|
switch central.state {
|
||||||
|
case .poweredOff:
|
||||||
|
break
|
||||||
|
case .poweredOn:
|
||||||
|
bluetoothIsAvailable = true
|
||||||
|
return
|
||||||
|
case .unsupported:
|
||||||
|
log.info("Bluetooth state: Not supported")
|
||||||
|
case .unknown:
|
||||||
|
log.info("Bluetooth state: Unknown")
|
||||||
|
case .resetting:
|
||||||
|
log.info("Bluetooth state: Resetting")
|
||||||
|
case .unauthorized:
|
||||||
|
log.info("Bluetooth state: Not authorized")
|
||||||
|
@unknown default:
|
||||||
|
log.warning("Bluetooth state: Unknown (\(central.state))")
|
||||||
|
}
|
||||||
|
bluetoothIsAvailable = false
|
||||||
|
// TODO: Disconnect devices?
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
|
log.info("Scanner: Connected to '\(peripheral.name ?? "No Name")'")
|
||||||
|
|
||||||
|
connectionState = .discoveringService
|
||||||
|
peripheral.delegate = self
|
||||||
|
peripheral.discoverServices([serviceUUID])
|
||||||
|
connectingDevice = peripheral
|
||||||
|
configuredDevice = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||||
|
log.info("Scanner: Disconnected from '\(peripheral.name ?? "No Name")'")
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
configuredDevice = nil
|
||||||
|
connectingDevice = nil
|
||||||
|
// TODO: Check if peripheral matches the connected device(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||||
|
log.warning("Scanner: Failed to connect to device '\(peripheral.name ?? "No Name")' (\(error?.localizedDescription ?? "No error"))")
|
||||||
|
isScanningForDevices = true
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
connectingDevice = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BluetoothScanner: CBPeripheralDelegate {
|
||||||
|
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||||
|
guard let services = peripheral.services, !services.isEmpty else {
|
||||||
|
log.error("Connected device '\(peripheral.name ?? "No Name")': No services found")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
connectingDevice = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
|
||||||
|
log.error("Connected device '\(peripheral.name ?? "No Name")': Required service not found: \(services.map { $0.uuid.uuidString})")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
connectingDevice = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peripheral.delegate = self
|
||||||
|
peripheral.discoverCharacteristics([characteristicUUID], for: service)
|
||||||
|
connectionState = .discoveringCharacteristic
|
||||||
|
connectingDevice = peripheral
|
||||||
|
}
|
||||||
|
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
log.error("Failed to discover characteristics: \(error)")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
connectingDevice = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
|
||||||
|
log.error("Connected device '\(peripheral.name ?? "No Name")': No characteristics found")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
connectingDevice = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var desiredCharacteristic: CBCharacteristic? = nil
|
||||||
|
for characteristic in characteristics {
|
||||||
|
guard characteristic.uuid == characteristicUUID else {
|
||||||
|
log.warning("Connected device '\(peripheral.name ?? "No Name")': Unused characteristic \(characteristic.uuid.uuidString)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
desiredCharacteristic = characteristic
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionState = .noDeviceFound
|
||||||
|
connectingDevice = nil
|
||||||
|
|
||||||
|
guard let desiredCharacteristic else {
|
||||||
|
log.error("Connected device '\(peripheral.name ?? "No Name")': Characteristic not found")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configuredDevice = .init(peripheral: peripheral, characteristic: desiredCharacteristic)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
|
/*
|
||||||
final class DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObject {
|
actor DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObject {
|
||||||
|
|
||||||
static let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
|
static let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
|
||||||
|
|
||||||
@ -149,10 +149,9 @@ final class DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
device.writeValue(requestData, for: characteristic, type: .withResponse)
|
device.writeValue(requestData, for: characteristic, type: .withResponse)
|
||||||
device.readValue(for: characteristic)
|
device.readValue(for: characteristic)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
|
||||||
guard let requestContinuation = self?.requestContinuation else { return }
|
Task {
|
||||||
log.info("Timed out for request \(request.type)")
|
await self?.checkTimeoutForCurrentRequest(request.type)
|
||||||
requestContinuation.resume(returning: nil)
|
}
|
||||||
self?.requestContinuation = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +182,21 @@ final class DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
return request.makeResponse(from: responseData.dropFirst(), responseType: responseType)
|
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) {
|
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 {
|
guard shouldConnectIfPossible else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -193,8 +206,15 @@ final class DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
state = .connecting(device: peripheral)
|
state = .connecting(device: peripheral)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
switch central.state {
|
Task {
|
||||||
|
await didUpdate(state: central.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didUpdate(state newState: CBManagerState) {
|
||||||
|
switch newState {
|
||||||
case .poweredOff:
|
case .poweredOff:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
case .poweredOn:
|
case .poweredOn:
|
||||||
@ -214,17 +234,31 @@ final class DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
log.info("Bluetooth is not authorized")
|
log.info("Bluetooth is not authorized")
|
||||||
@unknown default:
|
@unknown default:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
log.warning("Unknown state \(central.state)")
|
log.warning("Unknown state \(newState)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
|
Task {
|
||||||
|
await didConnect(to: peripheral)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didConnect(to peripheral: CBPeripheral) {
|
||||||
log.info("Connected to " + peripheral.name!)
|
log.info("Connected to " + peripheral.name!)
|
||||||
peripheral.discoverServices([DeviceManager.serviceUUID])
|
peripheral.discoverServices([DeviceManager.serviceUUID])
|
||||||
state = .discoveringServices(device: peripheral)
|
state = .discoveringServices(device: peripheral)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
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!)
|
log.info("Disconnected from " + peripheral.name!)
|
||||||
state = .disconnected
|
state = .disconnected
|
||||||
// Attempt to reconnect
|
// Attempt to reconnect
|
||||||
@ -233,7 +267,14 @@ final class DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
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")'")
|
log.warning("Failed to connect device '\(peripheral.name ?? "NO_NAME")'")
|
||||||
if let error = error {
|
if let error = error {
|
||||||
log.warning(error.localizedDescription)
|
log.warning(error.localizedDescription)
|
||||||
@ -248,7 +289,14 @@ final class DeviceConnection: NSObject, CBCentralManagerDelegate, ObservableObje
|
|||||||
|
|
||||||
extension DeviceConnection: CBPeripheralDelegate {
|
extension DeviceConnection: CBPeripheralDelegate {
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
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 {
|
guard let services = peripheral.services, !services.isEmpty else {
|
||||||
log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'")
|
log.error("No services found for device '\(peripheral.name ?? "NO_NAME")'")
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
@ -263,7 +311,14 @@ extension DeviceConnection: CBPeripheralDelegate {
|
|||||||
state = .discoveringCharacteristic(device: peripheral)
|
state = .discoveringCharacteristic(device: peripheral)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
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 {
|
if let error = error {
|
||||||
log.error("Failed to discover characteristics: \(error)")
|
log.error("Failed to discover characteristics: \(error)")
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
@ -284,22 +339,37 @@ extension DeviceConnection: CBPeripheralDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
|
log.error("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated
|
||||||
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
log.warning("Failed to get RSSI: \(error)")
|
log.warning("Failed to get RSSI: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastRSSI = RSSI.intValue
|
Task {
|
||||||
log.info("RSSI: \(lastRSSI)")
|
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?) {
|
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 {
|
if let error = error {
|
||||||
log.error("Failed to read value update: \(error)")
|
log.error("Failed to read value update: \(error)")
|
||||||
continueRequest(with: nil)
|
continueRequest(with: nil)
|
||||||
@ -332,3 +402,4 @@ extension DeviceConnection: CBPeripheralDelegate {
|
|||||||
self.requestContinuation = nil
|
self.requestContinuation = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
@ -3,6 +3,8 @@ import SFSafeSymbols
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
|
private let deviceInfoUpdateInterval = 3.0
|
||||||
|
|
||||||
private let minTempColor = Color(hue: 0.624, saturation: 0.5, brightness: 1.0)
|
private let minTempColor = Color(hue: 0.624, saturation: 0.5, brightness: 1.0)
|
||||||
private let minTemperature = -20.0
|
private let minTemperature = -20.0
|
||||||
|
|
||||||
@ -11,11 +13,11 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private let disconnectedColor = Color(white: 0.8)
|
private let disconnectedColor = Color(white: 0.8)
|
||||||
|
|
||||||
@EnvironmentObject
|
@StateObject
|
||||||
var bluetoothClient: BluetoothClient
|
var scanner = BluetoothScanner()
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var storage: TemperatureStorage
|
var storage: PersistentStorage
|
||||||
|
|
||||||
@State
|
@State
|
||||||
var showDeviceInfo = false
|
var showDeviceInfo = false
|
||||||
@ -26,13 +28,23 @@ struct ContentView: View {
|
|||||||
@State
|
@State
|
||||||
var showLog = false
|
var showLog = false
|
||||||
|
|
||||||
init() {
|
@State
|
||||||
|
var showDataTransferView = false
|
||||||
|
|
||||||
}
|
@State
|
||||||
|
var deviceInfoUpdateTimer: Timer?
|
||||||
|
|
||||||
|
init() { }
|
||||||
|
|
||||||
var averageTemperature: Double? {
|
var averageTemperature: Double? {
|
||||||
let t1 = bluetoothClient.deviceInfo?.sensor1?.optionalValue
|
guard let bluetoothDevice = scanner.configuredDevice else {
|
||||||
guard let t0 = bluetoothClient.deviceInfo?.sensor0?.optionalValue else {
|
return nil
|
||||||
|
}
|
||||||
|
guard let info = bluetoothDevice.lastDeviceInfo else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let t1 = info.sensor1?.optionalValue
|
||||||
|
guard let t0 = info.sensor0?.optionalValue else {
|
||||||
return t1
|
return t1
|
||||||
}
|
}
|
||||||
guard let t1 else {
|
guard let t1 else {
|
||||||
@ -87,15 +99,48 @@ struct ContentView: View {
|
|||||||
return .init(colors: [lighter, color])
|
return .init(colors: [lighter, color])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var connectionSymbol: SFSymbol {
|
||||||
|
if scanner.configuredDevice != nil {
|
||||||
|
return .iphoneCircle
|
||||||
|
}
|
||||||
|
if !scanner.bluetoothIsAvailable {
|
||||||
|
return .antennaRadiowavesLeftAndRightSlash
|
||||||
|
}
|
||||||
|
switch scanner.connectionState {
|
||||||
|
case .noDeviceFound:
|
||||||
|
if scanner.isScanningForDevices {
|
||||||
|
return .iphoneRadiowavesLeftAndRightCircle
|
||||||
|
}
|
||||||
|
return .iphoneSlashCircle
|
||||||
|
case .connecting:
|
||||||
|
return .arrowRightToLineCircle
|
||||||
|
case .discoveringService:
|
||||||
|
return .linkCircle
|
||||||
|
case .discoveringCharacteristic:
|
||||||
|
return .magnifyingglassCircle
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNoDeviceInfo: Bool {
|
||||||
|
scanner.configuredDevice?.lastDeviceInfo == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDisconnected: Bool {
|
||||||
|
scanner.configuredDevice == nil
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
// Image(systemSymbol: temperatureIcon)
|
|
||||||
// .font(.system(size: 100, weight: .light))
|
|
||||||
if hasTemperature {
|
if hasTemperature {
|
||||||
Text(temperatureString)
|
Text(temperatureString)
|
||||||
.font(.system(size: 150, weight: .light))
|
.font(.system(size: 150, weight: .light))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
} else {
|
||||||
|
Image(systemSymbol: temperatureIcon)
|
||||||
|
.font(.system(size: 100, weight: .thin))
|
||||||
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -103,45 +148,63 @@ struct ContentView: View {
|
|||||||
Button {
|
Button {
|
||||||
self.showHistory = true
|
self.showHistory = true
|
||||||
} label: {
|
} label: {
|
||||||
TemperatureHistoryChart(points: $storage.recentMeasurements)
|
ZStack {
|
||||||
.frame(height: 300)
|
TemperatureHistoryChart(points: $storage.recentMeasurements)
|
||||||
.background(Color.white.opacity(0.1))
|
.frame(height: 300)
|
||||||
.cornerRadius(8)
|
.background(Color.white.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
|
if storage.recentMeasurements.isEmpty {
|
||||||
|
Text("No recent measurements")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Button {
|
Button {
|
||||||
self.showLog.toggle()
|
self.showLog = true
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemSymbol: .textBubble)
|
Image(systemSymbol: .paperclipCircle)
|
||||||
.font(.system(size: 30, weight: .light))
|
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
self.showDeviceInfo = true
|
self.scanner.isScanningForDevices.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
if bluetoothClient.hasInfo {
|
Image(systemSymbol: connectionSymbol)
|
||||||
Image(systemSymbol: .iphone)
|
.foregroundColor(.white)
|
||||||
.font(.system(size: 30, weight: .light))
|
|
||||||
}
|
|
||||||
Text(bluetoothClient.deviceState.text)
|
|
||||||
}
|
}
|
||||||
.disabled(!bluetoothClient.hasInfo)
|
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
if let device = scanner.configuredDevice {
|
||||||
bluetoothClient.collectRecordedData()
|
Button {
|
||||||
} label: {
|
self.showDeviceInfo = true
|
||||||
|
} label: {
|
||||||
|
Image(systemSymbol: .infoCircle)
|
||||||
|
.foregroundColor(device.lastDeviceInfo == nil ? .gray : .white)
|
||||||
|
}.disabled(device.lastDeviceInfo == nil)
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
showDataTransferView = true
|
||||||
|
} label: {
|
||||||
|
Image(systemSymbol: .arrowUpArrowDownCircle)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemSymbol: .infoCircle)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
Spacer()
|
||||||
Image(systemSymbol: .arrowUpArrowDownCircle)
|
Image(systemSymbol: .arrowUpArrowDownCircle)
|
||||||
.font(.system(size: 30, weight: .light))
|
.foregroundColor(.gray)
|
||||||
.foregroundColor(.white)
|
}
|
||||||
}.disabled(!bluetoothClient.isConnected)
|
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.font(.system(size: 30, weight: .light))
|
||||||
|
|
||||||
}.padding()
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.sheet(isPresented: $showDeviceInfo) {
|
.sheet(isPresented: $showDeviceInfo) {
|
||||||
if let info = bluetoothClient.deviceInfo {
|
if let info = scanner.configuredDevice?.lastDeviceInfo {
|
||||||
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
|
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
@ -155,17 +218,53 @@ struct ContentView: View {
|
|||||||
LogView()
|
LogView()
|
||||||
.environmentObject(log)
|
.environmentObject(log)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showDataTransferView) {
|
||||||
|
if let client = scanner.configuredDevice {
|
||||||
|
TransferView(
|
||||||
|
bluetoothClient: client)
|
||||||
|
.environmentObject(storage)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
.background(backgroundGradient)
|
.background(backgroundGradient)
|
||||||
|
.onAppear(perform: startDeviceInfoUpdates)
|
||||||
|
.onDisappear(perform: endDeviceInfoUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Device info updates
|
||||||
|
|
||||||
|
private func startDeviceInfoUpdates() {
|
||||||
|
deviceInfoUpdateTimer?.invalidate()
|
||||||
|
|
||||||
|
log.info("Starting device info updates")
|
||||||
|
deviceInfoUpdateTimer = Timer.scheduledTimer(withTimeInterval: deviceInfoUpdateInterval, repeats: true) { timer in
|
||||||
|
self.updateDeviceInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceInfoUpdateTimer?.fire()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateDeviceInfo() {
|
||||||
|
guard let bluetoothDevice = scanner.configuredDevice else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
await bluetoothDevice.updateInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endDeviceInfoUpdates() {
|
||||||
|
deviceInfoUpdateTimer?.invalidate()
|
||||||
|
deviceInfoUpdateTimer = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let storage = TemperatureStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
||||||
let client = BluetoothClient(storage: storage, deviceInfo: .mock)
|
|
||||||
ContentView()
|
ContentView()
|
||||||
.environmentObject(storage)
|
.environmentObject(storage)
|
||||||
.environmentObject(client)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import Combine
|
|||||||
import BinaryCodable
|
import BinaryCodable
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class TemperatureStorage: ObservableObject {
|
final class PersistentStorage: ObservableObject {
|
||||||
|
|
||||||
static var documentDirectory: URL {
|
static var documentDirectory: URL {
|
||||||
try! FileManager.default.url(
|
try! FileManager.default.url(
|
||||||
@ -15,6 +15,9 @@ final class TemperatureStorage: 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.
|
||||||
|
|
||||||
@ -35,21 +38,25 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
var dailyMeasurementCounts: [MeasurementDailyCount] = []
|
var dailyMeasurementCounts: [MeasurementDailyCount] = []
|
||||||
|
|
||||||
|
/// The formatter for the temperature measurement file names
|
||||||
private let fileNameFormatter: DateFormatter
|
private let fileNameFormatter: DateFormatter
|
||||||
|
|
||||||
private let storageFolder: URL
|
/// The storage of daily temperature measurements
|
||||||
|
private let temperatureStorageFolderUrl: URL
|
||||||
|
|
||||||
private let overviewFileUrl: URL
|
/// The storage of the measurement counts per day
|
||||||
|
private let dailyCountsFileUrl: URL
|
||||||
|
|
||||||
private let fm: FileManager
|
private let fm: FileManager
|
||||||
|
|
||||||
|
/// The interval in which the measurements should be kept in `recentMeasurements`
|
||||||
private let lastValueInterval: TimeInterval
|
private let lastValueInterval: TimeInterval
|
||||||
|
|
||||||
init(lastMeasurements: [TemperatureMeasurement] = [], lastValueInterval: TimeInterval = 3600) {
|
init(lastMeasurements: [TemperatureMeasurement] = [], lastValueInterval: TimeInterval = 3600) {
|
||||||
self.recentMeasurements = lastMeasurements
|
self.recentMeasurements = lastMeasurements
|
||||||
let documentDirectory = TemperatureStorage.documentDirectory
|
let documentDirectory = PersistentStorage.documentDirectory
|
||||||
self.storageFolder = documentDirectory.appendingPathComponent("measurements")
|
self.temperatureStorageFolderUrl = documentDirectory.appendingPathComponent("measurements")
|
||||||
self.overviewFileUrl = documentDirectory.appendingPathComponent("overview.bin")
|
self.dailyCountsFileUrl = documentDirectory.appendingPathComponent("overview.bin")
|
||||||
self.fm = .default
|
self.fm = .default
|
||||||
self.fileNameFormatter = DateFormatter()
|
self.fileNameFormatter = DateFormatter()
|
||||||
self.fileNameFormatter.dateFormat = "yyyyMMdd.bin"
|
self.fileNameFormatter.dateFormat = "yyyyMMdd.bin"
|
||||||
@ -63,15 +70,15 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureExistenceOfFolder()
|
ensureExistenceOfFolder()
|
||||||
recalculateDailyCounts()
|
//recalculateDailyCounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ensureExistenceOfFolder() {
|
private func ensureExistenceOfFolder() {
|
||||||
guard !fm.fileExists(atPath: storageFolder.path) else {
|
guard !fm.fileExists(atPath: temperatureStorageFolderUrl.path) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try fm.createDirectory(at: storageFolder, withIntermediateDirectories: true)
|
try fm.createDirectory(at: temperatureStorageFolderUrl, withIntermediateDirectories: true)
|
||||||
} catch {
|
} catch {
|
||||||
log.error("Failed to create folder: \(error)")
|
log.error("Failed to create folder: \(error)")
|
||||||
}
|
}
|
||||||
@ -86,16 +93,17 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func fileUrl(for dateIndex: Int) -> URL {
|
private func fileUrl(for dateIndex: Int) -> URL {
|
||||||
storageFolder.appendingPathComponent(fileName(for: dateIndex))
|
temperatureStorageFolderUrl.appendingPathComponent(fileName(for: dateIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fileUrl(for fileName: String) -> URL {
|
private func fileUrl(for fileName: String) -> URL {
|
||||||
storageFolder.appendingPathComponent(fileName)
|
temperatureStorageFolderUrl.appendingPathComponent(fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadLastMeasurements() {
|
private func loadLastMeasurements() {
|
||||||
let startDate = Date().addingTimeInterval(-lastValueInterval)
|
let now = Date.now
|
||||||
let todayIndex = Date().dateIndex
|
let startDate = now.addingTimeInterval(-lastValueInterval)
|
||||||
|
let todayIndex = now.dateIndex
|
||||||
let todayValues = loadMeasurements(for: todayIndex)
|
let todayValues = loadMeasurements(for: todayIndex)
|
||||||
.filter { $0.date >= startDate }
|
.filter { $0.date >= startDate }
|
||||||
let dateIndexOfStart = startDate.dateIndex
|
let dateIndexOfStart = startDate.dateIndex
|
||||||
@ -220,7 +228,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
|
|
||||||
private func loadDailyCounts() {
|
private func loadDailyCounts() {
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: overviewFileUrl)
|
let data = try Data(contentsOf: dailyCountsFileUrl)
|
||||||
dailyMeasurementCounts = try BinaryDecoder.decode(from: data)
|
dailyMeasurementCounts = try BinaryDecoder.decode(from: data)
|
||||||
} catch {
|
} catch {
|
||||||
log.error("Failed to load overview: \(error)")
|
log.error("Failed to load overview: \(error)")
|
||||||
@ -230,7 +238,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
private func saveDailyCounts() {
|
private func saveDailyCounts() {
|
||||||
do {
|
do {
|
||||||
let data = try BinaryEncoder.encode(dailyMeasurementCounts)
|
let data = try BinaryEncoder.encode(dailyMeasurementCounts)
|
||||||
try data.write(to: overviewFileUrl)
|
try data.write(to: dailyCountsFileUrl)
|
||||||
} catch {
|
} catch {
|
||||||
log.error("Failed to write overview: \(error)")
|
log.error("Failed to write overview: \(error)")
|
||||||
}
|
}
|
||||||
@ -248,7 +256,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
|
|
||||||
func recalculateDailyCounts() {
|
func recalculateDailyCounts() {
|
||||||
do {
|
do {
|
||||||
let files = try fm.contentsOfDirectory(atPath: storageFolder.path)
|
let files = try fm.contentsOfDirectory(atPath: temperatureStorageFolderUrl.path)
|
||||||
let newValues: [Int: Int] = files
|
let newValues: [Int: Int] = files
|
||||||
.reduce(into: [:]) { counts, fileName in
|
.reduce(into: [:]) { counts, fileName in
|
||||||
let dateString = fileName.replacingOccurrences(of: ".bin", with: "")
|
let dateString = fileName.replacingOccurrences(of: ".bin", with: "")
|
||||||
@ -269,6 +277,37 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
log.error("Failed to load daily counts: \(error)")
|
log.error("Failed to load daily counts: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Device time
|
||||||
|
|
||||||
|
var lastDeviceTime: DeviceTime? {
|
||||||
|
get {
|
||||||
|
guard let data = lastDeviceTimeData else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let result: DeviceTime = try BinaryDecoder.decode(from: data)
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
log.error("Failed to decode device time: \(error)")
|
||||||
|
lastDeviceTimeData = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
guard let newValue else {
|
||||||
|
lastDeviceTimeData = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let data = try BinaryEncoder.encode(newValue)
|
||||||
|
lastDeviceTimeData = data
|
||||||
|
} catch {
|
||||||
|
log.error("Failed to encode device time: \(error)")
|
||||||
|
lastDeviceTimeData = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Array where Element == TemperatureMeasurement {
|
private extension Array where Element == TemperatureMeasurement {
|
||||||
@ -294,9 +333,9 @@ private extension Array where Element == TemperatureMeasurement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TemperatureStorage {
|
extension PersistentStorage {
|
||||||
|
|
||||||
static var mock: TemperatureStorage {
|
static var mock: PersistentStorage {
|
||||||
.init(lastMeasurements: TemperatureMeasurement.mockData)
|
.init(lastMeasurements: TemperatureMeasurement.mockData)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
private let storage = TemperatureStorage()
|
private let storage = PersistentStorage()
|
||||||
private let bluetoothClient: BluetoothClient = {
|
|
||||||
.init(storage: storage)
|
|
||||||
}()
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct TempTrackApp: App {
|
struct TempTrackApp: App {
|
||||||
@ -12,7 +9,6 @@ struct TempTrackApp: App {
|
|||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
.environmentObject(storage)
|
.environmentObject(storage)
|
||||||
.environmentObject(bluetoothClient)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,36 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
/*
|
||||||
final class TemperatureDataTransfer {
|
final class TemperatureDataTransfer {
|
||||||
|
|
||||||
private let startDateOfCurrentTransfer: Date
|
private let startDateOfCurrentTransfer: Date
|
||||||
|
|
||||||
private let interval: Int
|
private var interval: TimeInterval {
|
||||||
|
TimeInterval(info.measurementInterval) * dilation
|
||||||
|
}
|
||||||
|
|
||||||
private var dataBuffer: Data = Data()
|
private var dataBuffer: Data = Data()
|
||||||
|
|
||||||
private(set) var currentByteIndex = 0
|
private(set) var currentByteIndex = 0
|
||||||
|
|
||||||
private(set) var size: Int
|
private var info: DeviceInfo
|
||||||
|
|
||||||
private(set) var blockSize: Int
|
private let dilation: Double
|
||||||
|
|
||||||
private var numberOfRecordingsInCurrentTransfer = 0
|
var size: Int {
|
||||||
|
info.numberOfRecordedBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockSize: Int {
|
||||||
|
min(50, info.transferBlockSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var numberOfRecordingsInCurrentTransfer: Int {
|
||||||
|
measurements.count
|
||||||
|
}
|
||||||
|
|
||||||
|
var time: DeviceTime {
|
||||||
|
info.time
|
||||||
|
}
|
||||||
|
|
||||||
var measurements: [TemperatureMeasurement] = []
|
var measurements: [TemperatureMeasurement] = []
|
||||||
|
|
||||||
@ -22,7 +38,7 @@ final class TemperatureDataTransfer {
|
|||||||
private var lastRecording: TemperatureMeasurement = .init(sensor0: .notFound, sensor1: .notFound, date: .now)
|
private var lastRecording: TemperatureMeasurement = .init(sensor0: .notFound, sensor1: .notFound, date: .now)
|
||||||
|
|
||||||
private var dateOfNextRecording: Date {
|
private var dateOfNextRecording: Date {
|
||||||
startDateOfCurrentTransfer.addingTimeInterval(TimeInterval(numberOfRecordingsInCurrentTransfer * interval))
|
startDateOfCurrentTransfer.addingTimeInterval(TimeInterval(numberOfRecordingsInCurrentTransfer) * interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
var unprocessedByteCount: Int {
|
var unprocessedByteCount: Int {
|
||||||
@ -33,39 +49,44 @@ final class TemperatureDataTransfer {
|
|||||||
size - currentByteIndex
|
size - currentByteIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
init(info: DeviceInfo) {
|
init(info: DeviceInfo, previous: DeviceTime?) {
|
||||||
self.interval = info.measurementInterval
|
let (estimatedStart, dilation) = info.estimatedTimeDilation(to: previous)
|
||||||
let recordingTime = info.numberOfStoredMeasurements * info.measurementInterval
|
log.info("Starting transfer")
|
||||||
self.startDateOfCurrentTransfer = info.nextMeasurement.addingTimeInterval(-TimeInterval(recordingTime))
|
log.info("Estimated start of recording: \(estimatedStart)")
|
||||||
self.size = info.numberOfRecordedBytes
|
log.info("Estimated time dilation: \(dilation)")
|
||||||
self.blockSize = info.transferBlockSize
|
self.info = info
|
||||||
|
self.dilation = dilation
|
||||||
|
self.startDateOfCurrentTransfer = estimatedStart
|
||||||
|
log.info("True measurement interval: \(interval)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(info: DeviceInfo) {
|
func update(info: DeviceInfo) {
|
||||||
self.size = info.numberOfRecordedBytes
|
self.info = info
|
||||||
self.blockSize = info.transferBlockSize
|
// Possible bug: Device time updated, but new measurement not transferred
|
||||||
|
// Future transfer will calculate wrong time
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextRequest() -> BluetoothRequest {
|
func nextRequest() -> BluetoothRequest {
|
||||||
guard remainingBytesToTransfer > 0 else {
|
guard remainingBytesToTransfer > 0 else {
|
||||||
return .clearRecordingBuffer(byteCount: size)
|
return .clearRecordingBuffer(byteCount: currentByteIndex)
|
||||||
}
|
}
|
||||||
let chunkSize = min(remainingBytesToTransfer, blockSize)
|
let chunkSize = min(remainingBytesToTransfer, blockSize)
|
||||||
return .getRecordingData(offset: currentByteIndex, count: chunkSize)
|
return .getRecordingData(offset: currentByteIndex, count: chunkSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(data: Data, offset: Int, count: Int) {
|
func add(data: Data, offset: Int, count: Int) -> Bool {
|
||||||
guard currentByteIndex == offset else {
|
guard currentByteIndex == offset else {
|
||||||
log.warning("Transfer: Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)")
|
log.warning("Transfer: Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
if data.count != count {
|
guard data.count == count else {
|
||||||
log.warning("Transfer: Expected \(count) bytes, received only \(data.count)")
|
log.warning("Transfer: Expected \(count) bytes, received only \(data.count)")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
dataBuffer.append(data)
|
dataBuffer.append(data)
|
||||||
currentByteIndex += data.count
|
currentByteIndex += data.count
|
||||||
processBytes()
|
log.info("Transfer: \(currentByteIndex) bytes (added \(data.count))")
|
||||||
log.info("Transfer: \(currentByteIndex) bytes (added \(data.count)), \(measurements.count) points")
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processBytes() {
|
private func processBytes() {
|
||||||
@ -76,7 +97,7 @@ final class TemperatureDataTransfer {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard dataBuffer.count >= 2 else {
|
guard dataBuffer.count >= 2 else {
|
||||||
// Wait for more data
|
// Missing data
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let temp0 = TemperatureValue(byte: dataBuffer.removeFirst())
|
let temp0 = TemperatureValue(byte: dataBuffer.removeFirst())
|
||||||
@ -85,16 +106,23 @@ final class TemperatureDataTransfer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeTransfer() {
|
func completeTransfer() -> Bool {
|
||||||
|
let emptyIndices = dataBuffer.enumerated().filter { $0.element == 0 }.map { $0.offset }
|
||||||
processBytes()
|
processBytes()
|
||||||
if !dataBuffer.isEmpty {
|
guard dataBuffer.isEmpty else {
|
||||||
log.warning("\(dataBuffer.count) bytes remaining in transfer buffer")
|
log.warning("\(dataBuffer.count) bytes remaining in transfer buffer")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
log.info("Transfer: \(currentByteIndex) bytes, \(measurements.count) points")
|
log.info("Transfer complete: \(currentByteIndex) bytes, \(measurements.count) points")
|
||||||
|
log.info("Empty bytes: \(emptyIndices)")
|
||||||
|
if measurements.count != info.numberOfStoredMeasurements {
|
||||||
|
log.warning("Decoded \(measurements.count) points, but only \(info.numberOfStoredMeasurements) recorded")
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addRelative(byte: UInt8) {
|
private func addRelative(byte: UInt8) {
|
||||||
add(sensor0: convertTemp(value: byte >> 4, relativeTo: lastRecording.sensor0),
|
add(sensor0: convertTemp(value: (byte >> 4) & 0x0F, relativeTo: lastRecording.sensor0),
|
||||||
sensor1: convertTemp(value: byte & 0x0F, relativeTo: lastRecording.sensor1))
|
sensor1: convertTemp(value: byte & 0x0F, relativeTo: lastRecording.sensor1))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +132,6 @@ final class TemperatureDataTransfer {
|
|||||||
sensor1: sensor1,
|
sensor1: sensor1,
|
||||||
date: dateOfNextRecording)
|
date: dateOfNextRecording)
|
||||||
|
|
||||||
numberOfRecordingsInCurrentTransfer += 1
|
|
||||||
if measurement.sensor0.isValid {
|
if measurement.sensor0.isValid {
|
||||||
lastRecording.sensor0 = measurement.sensor0
|
lastRecording.sensor0 = measurement.sensor0
|
||||||
}
|
}
|
||||||
@ -134,3 +161,4 @@ private extension TemperatureValue {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
import Foundation
|
@ -12,7 +12,7 @@ struct DayView: View {
|
|||||||
let dateIndex: Int
|
let dateIndex: Int
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var storage: TemperatureStorage
|
var storage: PersistentStorage
|
||||||
|
|
||||||
var entries: [TemperatureMeasurement] {
|
var entries: [TemperatureMeasurement] {
|
||||||
storage.loadMeasurements(for: dateIndex)
|
storage.loadMeasurements(for: dateIndex)
|
||||||
@ -33,6 +33,6 @@ struct DayView: View {
|
|||||||
struct DayView_Previews: PreviewProvider {
|
struct DayView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
DayView(dateIndex: Date.now.dateIndex)
|
DayView(dateIndex: Date.now.dateIndex)
|
||||||
.environmentObject(TemperatureStorage.mock)
|
.environmentObject(PersistentStorage.mock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ struct DeviceInfoView: View {
|
|||||||
var isPresented: Bool
|
var isPresented: Bool
|
||||||
|
|
||||||
private var runTimeString: String {
|
private var runTimeString: String {
|
||||||
let number = info.numberOfSecondsRunning
|
let number = info.time.secondsSincePowerOn
|
||||||
guard number >= 60 else {
|
guard number >= 60 else {
|
||||||
return "\(number) s"
|
return "\(number) s"
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ struct DeviceInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var nextUpdateText: String {
|
private var nextUpdateText: String {
|
||||||
let secs = Int(info.nextMeasurement.timeIntervalSinceNow.rounded())
|
let secs = info.time.nextMeasurement.secondsToNow
|
||||||
guard secs > 1 else {
|
guard secs > 1 else {
|
||||||
return "Now"
|
return "Now"
|
||||||
}
|
}
|
||||||
@ -71,42 +71,25 @@ struct DeviceInfoView: View {
|
|||||||
Text("Sensor \(id)")
|
Text("Sensor \(id)")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
if let sensor {
|
if let sensor {
|
||||||
HStack {
|
IconAndTextView(
|
||||||
Image(systemSymbol: sensor.temperatureIcon)
|
icon: sensor.temperatureIcon,
|
||||||
.frame(width: 30)
|
text: "\(sensor.temperatureText) (\(sensor.updateText))")
|
||||||
Text("\(sensor.temperatureText) (\(sensor.updateText))")
|
IconAndTextView(
|
||||||
}
|
icon: .tag,
|
||||||
HStack {
|
text: sensor.hexAddress)
|
||||||
Image(systemSymbol: .tag)
|
|
||||||
.frame(width: 30)
|
|
||||||
Text(sensor.hexAddress)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
HStack {
|
IconAndTextView(
|
||||||
Image(systemSymbol: .thermometerMediumSlash)
|
icon: .thermometerMediumSlash,
|
||||||
.frame(width: 30)
|
text: "Not connected")
|
||||||
Text("Not connected")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var updateText: String {
|
var updateText: String {
|
||||||
guard info.receivedDate.secondsToNow > 3 else {
|
guard info.time.date.secondsToNow > 3 else {
|
||||||
return "Updated Now"
|
return "Updated Now"
|
||||||
}
|
}
|
||||||
return "Updated \(info.receivedDate.timePassedText)"
|
return "Updated \(info.time.date.timePassedText)"
|
||||||
}
|
|
||||||
|
|
||||||
var clockOffsetText: String {
|
|
||||||
guard info.hasDeviceStartTimeSet else {
|
|
||||||
return "Clock not synchronized"
|
|
||||||
}
|
|
||||||
let offset = info.clockOffset.roundedInt
|
|
||||||
guard abs(offset) > 1 else {
|
|
||||||
return "No clock offset"
|
|
||||||
}
|
|
||||||
return "Offset: \(offset) s"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -115,52 +98,32 @@ struct DeviceInfoView: View {
|
|||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("System")
|
Text("System")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
HStack {
|
IconAndTextView(
|
||||||
Image(systemSymbol: .power)
|
icon: .power,
|
||||||
.frame(width: 30)
|
text: "\(df.string(from: info.time.deviceStartTime)) (\(runTimeString))")
|
||||||
Text("\(df.string(from: info.deviceStartTime)) (\(runTimeString))")
|
IconAndTextView(
|
||||||
Spacer()
|
icon: .autostartstop,
|
||||||
}
|
text: "Wakeup: \(info.wakeupReason.text)")
|
||||||
HStack {
|
|
||||||
Image(systemSymbol: .clockBadgeExclamationmark)
|
|
||||||
.frame(width: 30)
|
|
||||||
Text(clockOffsetText)
|
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
Image(systemSymbol: .autostartstop)
|
|
||||||
.frame(width: 30)
|
|
||||||
Text("Wakeup: \(info.wakeupReason.text)")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("Recording")
|
Text("Recording")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
HStack {
|
IconAndTextView(
|
||||||
Image(systemSymbol: .stopwatch)
|
icon: .stopwatch,
|
||||||
.frame(width: 30)
|
text: "\(nextUpdateText) (Every \(info.measurementInterval) s)")
|
||||||
Text("\(nextUpdateText) (Every \(info.measurementInterval) s)")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text("Storage")
|
Text("Storage")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
HStack {
|
IconAndTextView(
|
||||||
Image(systemSymbol: .speedometer)
|
icon: .speedometer,
|
||||||
.frame(width: 30)
|
text: "\(info.numberOfStoredMeasurements) Measurements (\(info.time.totalNumberOfMeasurements) total)")
|
||||||
Text("\(info.numberOfStoredMeasurements) Measurements (\(info.totalNumberOfMeasurements) total)")
|
IconAndTextView(
|
||||||
}
|
icon: storageIcon,
|
||||||
HStack {
|
text: storageText)
|
||||||
Image(systemSymbol: storageIcon)
|
IconAndTextView(
|
||||||
.frame(width: 30)
|
icon: .iphoneAndArrowForward,
|
||||||
Text(storageText)
|
text: "\(info.transferBlockSize) Byte Block Size")
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
Image(systemSymbol: .iphoneAndArrowForward)
|
|
||||||
.frame(width: 30)
|
|
||||||
Text("\(info.transferBlockSize) Byte Block Size")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sensorView(info.sensor0, id: 0)
|
sensorView(info.sensor0, id: 0)
|
||||||
sensorView(info.sensor1, id: 1)
|
sensorView(info.sensor1, id: 1)
|
||||||
|
@ -3,7 +3,7 @@ import SwiftUI
|
|||||||
struct HistoryList: View {
|
struct HistoryList: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var storage: TemperatureStorage
|
var storage: PersistentStorage
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
@ -36,6 +36,6 @@ struct HistoryList: View {
|
|||||||
struct HistoryList_Previews: PreviewProvider {
|
struct HistoryList_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HistoryList()
|
HistoryList()
|
||||||
.environmentObject(TemperatureStorage(lastMeasurements: TemperatureMeasurement.mockData))
|
.environmentObject(PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
TempTrack/Views/IconAndTextView.swift
Normal file
24
TempTrack/Views/IconAndTextView.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct IconAndTextView: View {
|
||||||
|
|
||||||
|
let icon: SFSymbol
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: icon)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(text)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IconAndTextView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
IconAndTextView(icon: .power, text: "Awake time")
|
||||||
|
}
|
||||||
|
}
|
@ -13,17 +13,20 @@ struct LogView: View {
|
|||||||
var log: Log
|
var log: Log
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(log.logEntries) { entry in
|
NavigationView {
|
||||||
VStack(alignment: .leading) {
|
List(log.logEntries) { entry in
|
||||||
HStack {
|
VStack(alignment: .leading) {
|
||||||
Text(entry.level.description)
|
HStack {
|
||||||
Spacer()
|
Text(entry.level.description)
|
||||||
Text(df.string(from: entry.date))
|
Spacer()
|
||||||
}.font(.footnote)
|
Text(df.string(from: entry.date))
|
||||||
Text(entry.message)
|
}.font(.footnote)
|
||||||
|
Text(entry.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle("Log")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ struct TemperatureDayOverview: View {
|
|||||||
self.points = points
|
self.points = points
|
||||||
}
|
}
|
||||||
|
|
||||||
init(storage: TemperatureStorage, dateIndex: Int) {
|
init(storage: PersistentStorage, dateIndex: Int) {
|
||||||
let points = storage.loadMeasurements(for: dateIndex)
|
let points = storage.loadMeasurements(for: dateIndex)
|
||||||
self.points = points
|
self.points = points
|
||||||
update()
|
update()
|
||||||
@ -77,7 +77,7 @@ struct TemperatureDayOverview: View {
|
|||||||
|
|
||||||
struct TemperatureDayOverview_Previews: PreviewProvider {
|
struct TemperatureDayOverview_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
TemperatureDayOverview(storage: TemperatureStorage.mock, dateIndex: Date().dateIndex)
|
TemperatureDayOverview(storage: PersistentStorage.mock, dateIndex: Date().dateIndex)
|
||||||
.previewLayout(.fixed(width: 350, height: 150))
|
.previewLayout(.fixed(width: 350, height: 150))
|
||||||
//.background(.gray)
|
//.background(.gray)
|
||||||
}
|
}
|
||||||
|
245
TempTrack/Views/TransferView.swift
Normal file
245
TempTrack/Views/TransferView.swift
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct TransferView: View {
|
||||||
|
|
||||||
|
private let storageWarnBytes = 500
|
||||||
|
|
||||||
|
|
||||||
|
let bluetoothClient: BluetoothDevice
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var storage: PersistentStorage
|
||||||
|
|
||||||
|
@State
|
||||||
|
var bytesTransferred: Double = 0.0
|
||||||
|
|
||||||
|
@State
|
||||||
|
var totalBytes: Double = 0.0
|
||||||
|
|
||||||
|
@State
|
||||||
|
var measurements: [TemperatureMeasurement] = []
|
||||||
|
|
||||||
|
@State
|
||||||
|
var transferIsRunning = false
|
||||||
|
|
||||||
|
|
||||||
|
private var storageIcon: SFSymbol {
|
||||||
|
guard let info = bluetoothClient.lastDeviceInfo else {
|
||||||
|
return .externaldrive
|
||||||
|
}
|
||||||
|
if info.storageSize - info.numberOfRecordedBytes < storageWarnBytes {
|
||||||
|
return .externaldriveTrianglebadgeExclamationmark
|
||||||
|
}
|
||||||
|
return .externaldrive
|
||||||
|
}
|
||||||
|
|
||||||
|
private var measurementsText: String {
|
||||||
|
guard let info = bluetoothClient.lastDeviceInfo else {
|
||||||
|
return "No measurements"
|
||||||
|
}
|
||||||
|
return "\(info.numberOfStoredMeasurements) measurements (\(info.time.totalNumberOfMeasurements) total)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var storageText: String {
|
||||||
|
guard let info = bluetoothClient.lastDeviceInfo else {
|
||||||
|
return "No data"
|
||||||
|
}
|
||||||
|
if info.storageSize <= 0 {
|
||||||
|
return "\(info.numberOfRecordedBytes) Bytes"
|
||||||
|
}
|
||||||
|
return "\(info.numberOfRecordedBytes) / \(info.storageSize) Bytes (\(info.storageFillPercentage) %)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var transferSizeText: String {
|
||||||
|
guard let info = bluetoothClient.lastDeviceInfo else {
|
||||||
|
return "No transfer size"
|
||||||
|
}
|
||||||
|
return "\(info.transferBlockSize) Byte Block Size"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var transferByteText: String {
|
||||||
|
let total = Int(totalBytes)
|
||||||
|
guard total > 0 else {
|
||||||
|
return "No data"
|
||||||
|
}
|
||||||
|
return "\(Int(bytesTransferred)) / \(total) Bytes"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var transferMeasurementText: String {
|
||||||
|
guard !measurements.isEmpty else {
|
||||||
|
return "No measurements"
|
||||||
|
}
|
||||||
|
return "\(measurements.count) measurements"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
VStack {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text("Storage")
|
||||||
|
.font(.headline)
|
||||||
|
IconAndTextView(
|
||||||
|
icon: .speedometer,
|
||||||
|
text: measurementsText)
|
||||||
|
IconAndTextView(
|
||||||
|
icon: storageIcon,
|
||||||
|
text: storageText)
|
||||||
|
IconAndTextView(
|
||||||
|
icon: .iphoneAndArrowForward,
|
||||||
|
text: transferSizeText)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: clearStorage) {
|
||||||
|
Text("Remove recorded data")
|
||||||
|
}
|
||||||
|
.disabled(transferIsRunning)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text("Transfer")
|
||||||
|
.font(.headline)
|
||||||
|
ProgressView(value: bytesTransferred, total: totalBytes)
|
||||||
|
.progressViewStyle(.linear)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
IconAndTextView(
|
||||||
|
icon: .externaldrive,
|
||||||
|
text: transferByteText)
|
||||||
|
IconAndTextView(
|
||||||
|
icon: .speedometer,
|
||||||
|
text: transferMeasurementText)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Button(action: transferData) {
|
||||||
|
Text("Transfer")
|
||||||
|
}
|
||||||
|
.disabled(transferIsRunning)
|
||||||
|
.padding()
|
||||||
|
Spacer()
|
||||||
|
Button(action: saveTransfer) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
.disabled(transferIsRunning || measurements.isEmpty)
|
||||||
|
.padding()
|
||||||
|
Spacer()
|
||||||
|
Button(action: discardTransfer) {
|
||||||
|
Text("Discard")
|
||||||
|
}
|
||||||
|
.disabled(transferIsRunning || measurements.isEmpty)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.navigationTitle("Data Transfer")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferData() {
|
||||||
|
guard let info = bluetoothClient.lastDeviceInfo else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
transferIsRunning = true
|
||||||
|
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() {
|
||||||
|
self.measurements = []
|
||||||
|
self.bytesTransferred = 0
|
||||||
|
self.totalBytes = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveTransfer() {
|
||||||
|
// TODO: Save
|
||||||
|
|
||||||
|
discardTransfer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearStorage() {
|
||||||
|
guard let byteCount = bluetoothClient.lastDeviceInfo?.numberOfRecordedBytes else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
guard await bluetoothClient.deleteDeviceData(byteCount: byteCount) else {
|
||||||
|
log.warning("Failed to delete data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.warning("Device storage cleared")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransferView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
||||||
|
TransferView(bluetoothClient: .init())
|
||||||
|
.environmentObject(storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TemperatureValue {
|
||||||
|
|
||||||
|
var relativeValue: Double {
|
||||||
|
if case .value(let double) = self {
|
||||||
|
return double
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user