Basic display of info
This commit is contained in:
parent
0f97bfc316
commit
6e0910e47f
@ -11,6 +11,21 @@
|
|||||||
88CDE0512A2508E900114294 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0502A2508E900114294 /* ContentView.swift */; };
|
88CDE0512A2508E900114294 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0502A2508E900114294 /* ContentView.swift */; };
|
||||||
88CDE0532A2508EA00114294 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0522A2508EA00114294 /* Assets.xcassets */; };
|
88CDE0532A2508EA00114294 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0522A2508EA00114294 /* Assets.xcassets */; };
|
||||||
88CDE0562A2508EA00114294 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0552A2508EA00114294 /* Preview Assets.xcassets */; };
|
88CDE0562A2508EA00114294 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88CDE0552A2508EA00114294 /* Preview Assets.xcassets */; };
|
||||||
|
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE05C2A250F3C00114294 /* DeviceManager.swift */; };
|
||||||
|
88CDE05F2A250F5200114294 /* DeviceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE05E2A250F5200114294 /* DeviceState.swift */; };
|
||||||
|
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0602A25108100114294 /* BluetoothClient.swift */; };
|
||||||
|
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */; };
|
||||||
|
88CDE0662A25D08F00114294 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 88CDE0652A25D08F00114294 /* SFSafeSymbols */; };
|
||||||
|
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0672A2698B400114294 /* TemperatureStorage.swift */; };
|
||||||
|
88CDE06B2A2899C900114294 /* BottomSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 88CDE06A2A2899C900114294 /* BottomSheet */; };
|
||||||
|
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE06C2A28A92000114294 /* DeviceInfo.swift */; };
|
||||||
|
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE06F2A28AEA300114294 /* TemperatureMeasurement.swift */; };
|
||||||
|
88CDE0722A28AEB900114294 /* TemperatureDataTransferDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0712A28AEB900114294 /* TemperatureDataTransferDelegate.swift */; };
|
||||||
|
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */; };
|
||||||
|
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0752A28AF0900114294 /* TemperatureValue.swift */; };
|
||||||
|
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */; };
|
||||||
|
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */; };
|
||||||
|
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -19,6 +34,19 @@
|
|||||||
88CDE0502A2508E900114294 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
88CDE0502A2508E900114294 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
88CDE0522A2508EA00114294 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
88CDE0522A2508EA00114294 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
88CDE0552A2508EA00114294 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
88CDE0552A2508EA00114294 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
88CDE05C2A250F3C00114294 /* DeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceManager.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE05E2A250F5200114294 /* DeviceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceState.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE0602A25108100114294 /* BluetoothClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothClient.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDataTransfer.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE0672A2698B400114294 /* TemperatureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureStorage.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>"; };
|
||||||
|
88CDE0712A28AEB900114294 /* TemperatureDataTransferDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureDataTransferDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagerDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE0752A28AF0900114294 /* TemperatureValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureValue.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureSensor.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRequest.swift; sourceTree = "<group>"; };
|
||||||
|
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -26,6 +54,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
88CDE0662A25D08F00114294 /* SFSafeSymbols in Frameworks */,
|
||||||
|
88CDE06B2A2899C900114294 /* BottomSheet in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -51,10 +81,18 @@
|
|||||||
88CDE04D2A2508E900114294 /* TempTrack */ = {
|
88CDE04D2A2508E900114294 /* TempTrack */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
88CDE07C2A28AFE700114294 /* Views */,
|
||||||
|
88CDE0792A28AF3E00114294 /* Bluetooth */,
|
||||||
|
88CDE06E2A28AE8D00114294 /* Temperature */,
|
||||||
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
88CDE04E2A2508E900114294 /* TempTrackApp.swift */,
|
||||||
|
88CDE05C2A250F3C00114294 /* DeviceManager.swift */,
|
||||||
|
88CDE0732A28AEE500114294 /* DeviceManagerDelegate.swift */,
|
||||||
|
88CDE05E2A250F5200114294 /* DeviceState.swift */,
|
||||||
88CDE0502A2508E900114294 /* ContentView.swift */,
|
88CDE0502A2508E900114294 /* ContentView.swift */,
|
||||||
88CDE0522A2508EA00114294 /* Assets.xcassets */,
|
88CDE0522A2508EA00114294 /* Assets.xcassets */,
|
||||||
88CDE0542A2508EA00114294 /* Preview Content */,
|
88CDE0542A2508EA00114294 /* Preview Content */,
|
||||||
|
88CDE0672A2698B400114294 /* TemperatureStorage.swift */,
|
||||||
|
88CDE06C2A28A92000114294 /* DeviceInfo.swift */,
|
||||||
);
|
);
|
||||||
path = TempTrack;
|
path = TempTrack;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -67,6 +105,35 @@
|
|||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
88CDE06E2A28AE8D00114294 /* Temperature */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
88CDE06F2A28AEA300114294 /* TemperatureMeasurement.swift */,
|
||||||
|
88CDE0712A28AEB900114294 /* TemperatureDataTransferDelegate.swift */,
|
||||||
|
88CDE0622A253AD900114294 /* TemperatureDataTransfer.swift */,
|
||||||
|
88CDE0752A28AF0900114294 /* TemperatureValue.swift */,
|
||||||
|
88CDE0772A28AF2C00114294 /* TemperatureSensor.swift */,
|
||||||
|
);
|
||||||
|
path = Temperature;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
88CDE0792A28AF3E00114294 /* Bluetooth */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
88CDE0602A25108100114294 /* BluetoothClient.swift */,
|
||||||
|
88CDE07A2A28AF5100114294 /* BluetoothRequest.swift */,
|
||||||
|
);
|
||||||
|
path = Bluetooth;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
88CDE07C2A28AFE700114294 /* Views */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */,
|
||||||
|
);
|
||||||
|
path = Views;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -83,6 +150,10 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = TempTrack;
|
name = TempTrack;
|
||||||
|
packageProductDependencies = (
|
||||||
|
88CDE0652A25D08F00114294 /* SFSafeSymbols */,
|
||||||
|
88CDE06A2A2899C900114294 /* BottomSheet */,
|
||||||
|
);
|
||||||
productName = TempTrack;
|
productName = TempTrack;
|
||||||
productReference = 88CDE04B2A2508E900114294 /* TempTrack.app */;
|
productReference = 88CDE04B2A2508E900114294 /* TempTrack.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -111,6 +182,10 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 88CDE0422A2508E800114294;
|
mainGroup = 88CDE0422A2508E800114294;
|
||||||
|
packageReferences = (
|
||||||
|
88CDE0642A25D08F00114294 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
|
88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */,
|
||||||
|
);
|
||||||
productRefGroup = 88CDE04C2A2508E900114294 /* Products */;
|
productRefGroup = 88CDE04C2A2508E900114294 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@ -138,7 +213,20 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
||||||
|
88CDE05F2A250F5200114294 /* DeviceState.swift in Sources */,
|
||||||
|
88CDE0632A253AD900114294 /* TemperatureDataTransfer.swift in Sources */,
|
||||||
|
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */,
|
||||||
|
88CDE05D2A250F3C00114294 /* DeviceManager.swift in Sources */,
|
||||||
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */,
|
88CDE04F2A2508E900114294 /* TempTrackApp.swift in Sources */,
|
||||||
|
88CDE0722A28AEB900114294 /* TemperatureDataTransferDelegate.swift in Sources */,
|
||||||
|
88CDE0782A28AF2C00114294 /* TemperatureSensor.swift in Sources */,
|
||||||
|
88CDE0682A2698B400114294 /* TemperatureStorage.swift in Sources */,
|
||||||
|
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
||||||
|
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
||||||
|
88CDE0612A25108100114294 /* BluetoothClient.swift in Sources */,
|
||||||
|
88CDE0742A28AEE500114294 /* DeviceManagerDelegate.swift in Sources */,
|
||||||
|
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
||||||
|
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -270,6 +358,8 @@
|
|||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Connect to loggers";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Connect to temperature loggers";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@ -299,6 +389,8 @@
|
|||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Connect to loggers";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Connect to temperature loggers";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@ -339,6 +431,38 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
88CDE0642A25D08F00114294 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 4.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/weitieda/bottom-sheet";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
88CDE0652A25D08F00114294 /* SFSafeSymbols */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 88CDE0642A25D08F00114294 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
|
productName = SFSafeSymbols;
|
||||||
|
};
|
||||||
|
88CDE06A2A2899C900114294 /* BottomSheet */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 88CDE0692A2899C900114294 /* XCRemoteSwiftPackageReference "bottom-sheet" */;
|
||||||
|
productName = BottomSheet;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 88CDE0432A2508E800114294 /* Project object */;
|
rootObject = 88CDE0432A2508E800114294 /* Project object */;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "bottom-sheet",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/weitieda/bottom-sheet",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6b21007153365235418f3943a960a1f9546592e0",
|
||||||
|
"version" : "1.0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sfsafesymbols",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
|
||||||
|
"version" : "4.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
204
TempTrack/Bluetooth/BluetoothClient.swift
Normal file
204
TempTrack/Bluetooth/BluetoothClient.swift
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum BluetoothResponseType: UInt8 {
|
||||||
|
|
||||||
|
/// The response to the last request is provided
|
||||||
|
case success = 0
|
||||||
|
|
||||||
|
/// Invalid command received
|
||||||
|
case invalidCommand = 1
|
||||||
|
|
||||||
|
case responseTooLarge = 2
|
||||||
|
|
||||||
|
case unknownCommand = 3
|
||||||
|
|
||||||
|
case invalidNumberOfBytesToDelete = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
final class BluetoothClient: ObservableObject {
|
||||||
|
|
||||||
|
private let updateInterval = 3.0
|
||||||
|
|
||||||
|
private let connection = DeviceManager()
|
||||||
|
|
||||||
|
private let recorder = TemperatureStorage()
|
||||||
|
|
||||||
|
init(deviceInfo: DeviceInfo? = nil) {
|
||||||
|
connection.delegate = self
|
||||||
|
self.deviceInfo = deviceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect() -> Bool {
|
||||||
|
connection.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
private(set) var deviceState: DeviceState = .disconnected {
|
||||||
|
didSet {
|
||||||
|
print("State: \(deviceState.text)")
|
||||||
|
if case .configured = deviceState {
|
||||||
|
startRegularUpdates()
|
||||||
|
} else {
|
||||||
|
endRegularUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
private(set) var deviceInfo: DeviceInfo?
|
||||||
|
|
||||||
|
private var openRequests: [BluetoothRequest] = []
|
||||||
|
|
||||||
|
private var runningRequest: BluetoothRequest?
|
||||||
|
|
||||||
|
private var runningTransfer: TemperatureDataTransfer?
|
||||||
|
|
||||||
|
func updateDeviceInfo() {
|
||||||
|
addRequest(.getInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dataUpdateTimer: Timer?
|
||||||
|
|
||||||
|
private func startRegularUpdates() {
|
||||||
|
guard dataUpdateTimer == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Starting updates")
|
||||||
|
dataUpdateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] timer in
|
||||||
|
guard let self = self else {
|
||||||
|
timer.invalidate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateDeviceInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
dataUpdateTimer?.fire()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endRegularUpdates() {
|
||||||
|
guard let dataUpdateTimer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataUpdateTimer.invalidate()
|
||||||
|
runningRequest = nil
|
||||||
|
self.dataUpdateTimer = nil
|
||||||
|
print("Ending updates")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performNextRequest() {
|
||||||
|
guard runningRequest == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !openRequests.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let next = openRequests.removeFirst()
|
||||||
|
//print("Starting request \(next)")
|
||||||
|
|
||||||
|
guard connection.send(next.serialized) else {
|
||||||
|
print("Failed to start request \(next)")
|
||||||
|
performNextRequest()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runningRequest = next
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRequest(_ request: BluetoothRequest) {
|
||||||
|
openRequests.append(request)
|
||||||
|
performNextRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func collectRecordedData() -> Bool {
|
||||||
|
guard let info = deviceInfo else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let transfer = TemperatureDataTransfer(info: info)
|
||||||
|
runningTransfer = transfer
|
||||||
|
runningTransfer?.delegate = recorder
|
||||||
|
let next = transfer.nextRequest()
|
||||||
|
addRequest(next)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didReceive(data: Data, offset: Int, count: Int) {
|
||||||
|
guard let runningTransfer else {
|
||||||
|
return // TODO: Start new transfer?
|
||||||
|
}
|
||||||
|
runningTransfer.add(data: data, offset: offset, count: count)
|
||||||
|
continueTransfer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func continueTransfer() {
|
||||||
|
guard let runningTransfer else {
|
||||||
|
return // TODO: Start new transfer?
|
||||||
|
}
|
||||||
|
let next = runningTransfer.nextRequest()
|
||||||
|
addRequest(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decode(info: Data) {
|
||||||
|
guard let newInfo = DeviceInfo(info: info) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.deviceInfo = newInfo
|
||||||
|
guard let runningTransfer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runningTransfer.update(info: newInfo)
|
||||||
|
let next = runningTransfer.nextRequest()
|
||||||
|
addRequest(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BluetoothClient: DeviceManagerDelegate {
|
||||||
|
|
||||||
|
func deviceManager(didReceive data: Data) {
|
||||||
|
defer {
|
||||||
|
self.runningRequest = nil
|
||||||
|
performNextRequest()
|
||||||
|
}
|
||||||
|
guard let runningRequest else {
|
||||||
|
print("No request active, but \(data) received")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard data.count > 0 else {
|
||||||
|
print("No response data for request \(runningRequest)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let type = BluetoothResponseType(rawValue: data[0]) else {
|
||||||
|
print("Unknown response \(data[0]) for request \(runningRequest)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard type == .success else {
|
||||||
|
print("Error response \(data[0]) for request \(runningRequest)")
|
||||||
|
// If clearing the recording buffer fails due to byte mismatch,
|
||||||
|
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||||
|
// If requesting bytes fails due to the response size,
|
||||||
|
// then requesting new info will update the response size, and the transfer will be resumed
|
||||||
|
addRequest(.getInfo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let payload = data.dropFirst()
|
||||||
|
|
||||||
|
switch runningRequest {
|
||||||
|
case .getInfo:
|
||||||
|
decode(info: payload)
|
||||||
|
case .getRecordingData(let offset, let count):
|
||||||
|
didReceive(data: payload, offset: offset, count: count)
|
||||||
|
case .clearRecordingBuffer:
|
||||||
|
runningTransfer?.completeTransfer()
|
||||||
|
runningTransfer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deviceManager(didChangeState state: DeviceState) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.deviceState = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
86
TempTrack/Bluetooth/BluetoothRequest.swift
Normal file
86
TempTrack/Bluetooth/BluetoothRequest.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum BluetoothRequest {
|
||||||
|
/**
|
||||||
|
* Request the number of bytes already recorded
|
||||||
|
*
|
||||||
|
* Request:
|
||||||
|
* - No additional bytes expected
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* - `BluetoothResponseType.success`
|
||||||
|
* - the number of recorded bytes as a `Uint16` (2 bytes)
|
||||||
|
* - the number of seconds until the next measurement as a `Uint16` (2 bytes)
|
||||||
|
* - the number of seconds between measurements as a `Uint16` (2 bytes)
|
||||||
|
* - the number of measurements as a `Uint16` (2 bytes)
|
||||||
|
* - the maximum number of bytes to request as a `Uint16` (2 bytes)
|
||||||
|
* - the number of seconds since power on as a `Uint32` (4 bytes)
|
||||||
|
*/
|
||||||
|
case getInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request recording data
|
||||||
|
*
|
||||||
|
* Request:
|
||||||
|
* - Bytes 1-2: Memory offset (`UInt16`)
|
||||||
|
* - Bytes 3-4: Number of bytes (`UInt16`)
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* - `BluetoothResponseType.success`, plus the requested bytes
|
||||||
|
* - `BluetoothResponseType.responseTooLarge` if too many bytes are requested
|
||||||
|
*/
|
||||||
|
case getRecordingData(offset: Int, count: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request deletion of recordings
|
||||||
|
*
|
||||||
|
* Request:
|
||||||
|
* - Bytes 1-2: Number of bytes to clear (uint16_t)
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* - `BluetoothResponseType.success`
|
||||||
|
* - `BluetoothResponseType.invalidNumberOfBytesToDelete`, if the number of bytes does not match.
|
||||||
|
* This may happen when a new temperature recording is performed in between calls
|
||||||
|
*/
|
||||||
|
case clearRecordingBuffer(byteCount: Int)
|
||||||
|
|
||||||
|
var serialized: Data {
|
||||||
|
let firstByte = Data([byte])
|
||||||
|
switch self {
|
||||||
|
case .getInfo:
|
||||||
|
return firstByte
|
||||||
|
case .getRecordingData(let offset, let count):
|
||||||
|
return firstByte + count.twoByteData + offset.twoByteData
|
||||||
|
case .clearRecordingBuffer(let byteCount):
|
||||||
|
return firstByte + byteCount.twoByteData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var byte: UInt8 {
|
||||||
|
switch self {
|
||||||
|
case .getInfo: return 0
|
||||||
|
case .getRecordingData: return 1
|
||||||
|
case .clearRecordingBuffer: return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Int {
|
||||||
|
|
||||||
|
var twoByteData: Data {
|
||||||
|
let value = UInt16(clamping: self)
|
||||||
|
return Data([value.low, value.high])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UInt16 {
|
||||||
|
|
||||||
|
var low: UInt8 {
|
||||||
|
UInt8(self & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
var high: UInt8 {
|
||||||
|
UInt8((self >> 8) & 0xFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,119 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// TempTrack
|
|
||||||
//
|
|
||||||
// Created by iMac on 29.05.23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
import BottomSheet
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var client = BluetoothClient()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init(client: BluetoothClient) {
|
||||||
|
self.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
@State
|
||||||
|
var showDeviceInfo = false
|
||||||
|
|
||||||
|
var averageTemperature: Double? {
|
||||||
|
let t1 = client.deviceInfo?.sensor1?.optionalValue
|
||||||
|
guard let t0 = client.deviceInfo?.sensor0?.optionalValue else {
|
||||||
|
return t1
|
||||||
|
}
|
||||||
|
guard let t1 else {
|
||||||
|
return t0
|
||||||
|
}
|
||||||
|
return (t0 + t1) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasTemperature: Bool {
|
||||||
|
averageTemperature != nil
|
||||||
|
}
|
||||||
|
var temperatureString: String {
|
||||||
|
guard let temp = averageTemperature else {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
return String(format: "%.0f°", locale: .current, temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var temperatureIcon: SFSymbol {
|
||||||
|
guard let temp = averageTemperature else {
|
||||||
|
return .thermometerMediumSlash
|
||||||
|
}
|
||||||
|
guard temp > 0 else {
|
||||||
|
return .thermometerSnowflake
|
||||||
|
}
|
||||||
|
guard temp > 15 else {
|
||||||
|
return .thermometerLow
|
||||||
|
}
|
||||||
|
guard temp > 25 else {
|
||||||
|
return .thermometerMedium
|
||||||
|
}
|
||||||
|
return .thermometerHigh
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Image(systemName: "globe")
|
HStack {
|
||||||
.imageScale(.large)
|
Image(systemSymbol: .iphone)
|
||||||
.foregroundColor(.accentColor)
|
.frame(width: 30)
|
||||||
Text("Hello, world!")
|
Text(client.deviceState.text)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Image(systemSymbol: temperatureIcon)
|
||||||
|
.font(.system(size: 200, weight: .light))
|
||||||
|
if hasTemperature {
|
||||||
|
Text(temperatureString)
|
||||||
|
.font(.system(size: 100, weight: .light))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Button(action: {
|
||||||
|
_ = client.collectRecordedData()
|
||||||
|
}) {
|
||||||
|
Text("Transfer")
|
||||||
|
}.padding()
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
self.showDeviceInfo = true
|
||||||
|
} label: {
|
||||||
|
Image(systemSymbol: .infoCircle)
|
||||||
|
.font(.system(size: 40, weight: .regular))
|
||||||
|
}.disabled(client.deviceInfo == nil)
|
||||||
|
}.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
.bottomSheet(isPresented: $showDeviceInfo, height: 520) {
|
||||||
|
if let info = client.deviceInfo {
|
||||||
|
DeviceInfoView(info: info)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView()
|
ContentView(client: BluetoothClient(deviceInfo: .mock))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension HorizontalAlignment {
|
||||||
|
|
||||||
|
private struct InfoTextAlignment: AlignmentID {
|
||||||
|
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||||
|
context[HorizontalAlignment.leading]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let infoTextAlignmentGuide = HorizontalAlignment(
|
||||||
|
InfoTextAlignment.self
|
||||||
|
)
|
||||||
|
}
|
||||||
|
103
TempTrack/DeviceInfo.swift
Normal file
103
TempTrack/DeviceInfo.swift
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DeviceInfo {
|
||||||
|
|
||||||
|
let receivedDate: Date
|
||||||
|
|
||||||
|
/// The number of bytes recorded by the tracker
|
||||||
|
let numberOfRecordedBytes: Int
|
||||||
|
|
||||||
|
/// The number of measurements already performed
|
||||||
|
let numberOfMeasurements: Int
|
||||||
|
|
||||||
|
/// The interval between measurements (in seconds)
|
||||||
|
let measurementInterval: Int
|
||||||
|
|
||||||
|
/// The maximum number of bytes which can be requested
|
||||||
|
let transferBlockSize: Int
|
||||||
|
|
||||||
|
let deviceStartTime: Date
|
||||||
|
|
||||||
|
let nextMeasurement: Date
|
||||||
|
|
||||||
|
let sensor0: TemperatureSensor?
|
||||||
|
|
||||||
|
let sensor1: TemperatureSensor?
|
||||||
|
|
||||||
|
let storageSize: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
The number of seconds the device has been powered on
|
||||||
|
*/
|
||||||
|
var numberOfSecondsRunning: Int {
|
||||||
|
Int(-deviceStartTime.timeIntervalSinceNow)
|
||||||
|
}
|
||||||
|
|
||||||
|
var storageFillRatio: Double {
|
||||||
|
Double(numberOfRecordedBytes) / Double(storageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var storageFillPercentage: Int {
|
||||||
|
Int((storageFillRatio * 100).rounded())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceInfo {
|
||||||
|
|
||||||
|
static var size = 38
|
||||||
|
|
||||||
|
init?(info: Data) {
|
||||||
|
guard info.count == DeviceInfo.size else {
|
||||||
|
print("Invalid info size \(info.count)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let data = Array(info)
|
||||||
|
self.receivedDate = Date()
|
||||||
|
self.numberOfRecordedBytes = .init(high: data[1], low: data[0])
|
||||||
|
let secondsUntilNextMeasurement = UInt16(high: data[3], low: data[2])
|
||||||
|
self.nextMeasurement = Date().addingTimeInterval(Double(secondsUntilNextMeasurement))
|
||||||
|
self.measurementInterval = .init(high: data[5], low: data[4])
|
||||||
|
self.numberOfMeasurements = .init(high: data[7], low: data[6])
|
||||||
|
self.transferBlockSize = .init(high: data[9], low: data[8])
|
||||||
|
let secondsSincePowerOn = Int(uint32: data[13], data[12], data[11], data[10])
|
||||||
|
self.deviceStartTime = Date().addingTimeInterval(Double(-secondsSincePowerOn))
|
||||||
|
self.sensor0 = .init(address: Array(data[16..<24]), valueByte: data[14], secondsAgo: UInt16(high: data[33], low: data[32]))
|
||||||
|
self.sensor1 = .init(address: Array(data[24..<32]), valueByte: data[15], secondsAgo: UInt16(high: data[35], low: data[34]))
|
||||||
|
self.storageSize = .init(high: data[37], low: data[36])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UInt16 {
|
||||||
|
|
||||||
|
init(high: UInt8, low: UInt8) {
|
||||||
|
self = UInt16(high) << 8 + UInt16(low)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Int {
|
||||||
|
|
||||||
|
init(uint32 byte0: UInt8, _ byte1: UInt8, _ byte2: UInt8, _ byte3: UInt8) {
|
||||||
|
self = (Int(byte0) << 24) | (Int(byte1) << 16) | (Int(byte2) << 8) | Int(byte3)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(high: UInt8, low: UInt8) {
|
||||||
|
self = Int(high) << 8 + Int(low)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceInfo {
|
||||||
|
|
||||||
|
static var mock: DeviceInfo {
|
||||||
|
.init(
|
||||||
|
receivedDate: Date(),
|
||||||
|
numberOfRecordedBytes: 123,
|
||||||
|
numberOfMeasurements: 234,
|
||||||
|
measurementInterval: 60,
|
||||||
|
transferBlockSize: 180,
|
||||||
|
deviceStartTime: .now.addingTimeInterval(-1000),
|
||||||
|
nextMeasurement: .now.addingTimeInterval(5),
|
||||||
|
sensor0: .init(address: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], value: .value(21.0), date: .now.addingTimeInterval(-2)),
|
||||||
|
sensor1: .init(address: [0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09], value: .value(19.0), date: .now.addingTimeInterval(-4)),
|
||||||
|
storageSize: 10000)
|
||||||
|
}
|
||||||
|
}
|
228
TempTrack/DeviceManager.swift
Normal file
228
TempTrack/DeviceManager.swift
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import Foundation
|
||||||
|
import CoreBluetooth
|
||||||
|
|
||||||
|
final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
||||||
|
|
||||||
|
static let serviceUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000001")
|
||||||
|
|
||||||
|
static let characteristicUUID = CBUUID(string: "22071991-cccc-cccc-cccc-000000000002")
|
||||||
|
|
||||||
|
private var manager: CBCentralManager! = nil
|
||||||
|
|
||||||
|
private(set) var lastRSSI: Int = 0
|
||||||
|
|
||||||
|
weak var delegate: DeviceManagerDelegate?
|
||||||
|
|
||||||
|
var state: DeviceState = .disconnected {
|
||||||
|
didSet {
|
||||||
|
delegate?.deviceManager(didChangeState: state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
self.manager = CBCentralManager(delegate: self, queue: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var dataUpdateTimer: Timer?
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func connect() -> Bool {
|
||||||
|
switch state {
|
||||||
|
case .bluetoothDisabled:
|
||||||
|
print("Can't connect, bluetooth disabled")
|
||||||
|
return false
|
||||||
|
case .disconnected, .bluetoothEnabled:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
guard !manager.isScanning else {
|
||||||
|
state = .scanning
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
shouldConnectIfPossible = true
|
||||||
|
state = .scanning
|
||||||
|
manager.scanForPeripherals(withServices: [DeviceManager.serviceUUID])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shouldConnectIfPossible = true
|
||||||
|
|
||||||
|
func disconnect() {
|
||||||
|
shouldConnectIfPossible = false
|
||||||
|
switch state {
|
||||||
|
case .bluetoothDisabled, .bluetoothEnabled:
|
||||||
|
return
|
||||||
|
case .scanning:
|
||||||
|
manager.stopScan()
|
||||||
|
state = .disconnected
|
||||||
|
return
|
||||||
|
case .connecting(let device),
|
||||||
|
.discoveringCharacteristic(let device),
|
||||||
|
.discoveringServices(device: let device),
|
||||||
|
.configured(let device, _):
|
||||||
|
manager.cancelPeripheralConnection(device)
|
||||||
|
manager.stopScan()
|
||||||
|
state = .disconnected
|
||||||
|
return
|
||||||
|
case .disconnected:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func send(_ data: Data) -> Bool {
|
||||||
|
guard case .configured(let device, let characteristic) = state else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
device.writeValue(data, for: characteristic, type: .withResponse)
|
||||||
|
//DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { }
|
||||||
|
return self.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func read() -> Bool {
|
||||||
|
guard case .configured(let device, let characteristic) = state else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
device.readValue(for: characteristic)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||||
|
//print("Found device '\(peripheral.name ?? "NO_NAME")'")
|
||||||
|
peripheral.delegate = self
|
||||||
|
manager.connect(peripheral)
|
||||||
|
manager.stopScan()
|
||||||
|
state = .connecting(device: peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
|
switch central.state {
|
||||||
|
case .poweredOff:
|
||||||
|
state = .bluetoothDisabled
|
||||||
|
case .poweredOn:
|
||||||
|
state = .bluetoothEnabled
|
||||||
|
connect()
|
||||||
|
case .unsupported:
|
||||||
|
state = .bluetoothDisabled
|
||||||
|
print("Bluetooth is not supported")
|
||||||
|
case .unknown:
|
||||||
|
state = .bluetoothDisabled
|
||||||
|
print("Bluetooth state is unknown")
|
||||||
|
case .resetting:
|
||||||
|
state = .bluetoothDisabled
|
||||||
|
print("Bluetooth is resetting")
|
||||||
|
case .unauthorized:
|
||||||
|
state = .bluetoothDisabled
|
||||||
|
print("Bluetooth is not authorized")
|
||||||
|
@unknown default:
|
||||||
|
state = .bluetoothDisabled
|
||||||
|
print("Unknown state \(central.state)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
|
//print("Connected to " + peripheral.name!)
|
||||||
|
peripheral.discoverServices([DeviceManager.serviceUUID])
|
||||||
|
state = .discoveringServices(device: peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||||
|
print("Disconnected from " + peripheral.name!)
|
||||||
|
state = .disconnected
|
||||||
|
// Attempt to reconnect
|
||||||
|
if shouldConnectIfPossible {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||||
|
print("Failed to connect device '\(peripheral.name ?? "NO_NAME")'")
|
||||||
|
if let error = error {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
state = manager.isScanning ? .scanning : .disconnected
|
||||||
|
// Attempt to reconnect
|
||||||
|
if shouldConnectIfPossible {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceManager: CBPeripheralDelegate {
|
||||||
|
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||||
|
guard let services = peripheral.services, !services.isEmpty else {
|
||||||
|
print("No services found for device '\(peripheral.name ?? "NO_NAME")'")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
|
||||||
|
print("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peripheral.discoverCharacteristics([DeviceManager.characteristicUUID], for: service)
|
||||||
|
state = .discoveringCharacteristic(device: peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("Failed to discover characteristics: \(error)")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
|
||||||
|
print("No characteristics found for device")
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for characteristic in characteristics {
|
||||||
|
guard characteristic.uuid == DeviceManager.characteristicUUID else {
|
||||||
|
print("Unused characteristic \(characteristic.uuid.uuidString)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state = .configured(device: peripheral, characteristic: characteristic)
|
||||||
|
peripheral.setNotifyValue(true, for: characteristic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
|
||||||
|
}
|
||||||
|
//print("Peripheral did write value for \(characteristic.uuid.uuidString)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("Failed to get RSSI: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastRSSI = RSSI.intValue
|
||||||
|
print("RSSI: \(lastRSSI)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("Failed to read value update: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case .configured(device: _, characteristic: let storedCharacteristic) = state else {
|
||||||
|
print("Received data while not properly configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard characteristic.uuid == storedCharacteristic.uuid else {
|
||||||
|
print("Read unknown characteristic \(characteristic.uuid.uuidString)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data = characteristic.value else {
|
||||||
|
print("No data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delegate?.deviceManager(didReceive: data)
|
||||||
|
}
|
||||||
|
}
|
8
TempTrack/DeviceManagerDelegate.swift
Normal file
8
TempTrack/DeviceManagerDelegate.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol DeviceManagerDelegate: AnyObject {
|
||||||
|
|
||||||
|
func deviceManager(didReceive data: Data)
|
||||||
|
|
||||||
|
func deviceManager(didChangeState state: DeviceState)
|
||||||
|
}
|
82
TempTrack/DeviceState.swift
Normal file
82
TempTrack/DeviceState.swift
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import Foundation
|
||||||
|
import CoreBluetooth
|
||||||
|
|
||||||
|
enum DeviceState {
|
||||||
|
|
||||||
|
case bluetoothDisabled
|
||||||
|
|
||||||
|
case bluetoothEnabled
|
||||||
|
|
||||||
|
case scanning
|
||||||
|
|
||||||
|
case connecting(device: CBPeripheral)
|
||||||
|
|
||||||
|
case discoveringServices(device: CBPeripheral)
|
||||||
|
|
||||||
|
case discoveringCharacteristic(device: CBPeripheral)
|
||||||
|
|
||||||
|
case configured(device: CBPeripheral, characteristic: CBCharacteristic)
|
||||||
|
|
||||||
|
case disconnected
|
||||||
|
|
||||||
|
var text: String {
|
||||||
|
switch self {
|
||||||
|
case .bluetoothDisabled:
|
||||||
|
return "Bluetooth is disabled"
|
||||||
|
case .bluetoothEnabled:
|
||||||
|
return "Bluetooth enabled"
|
||||||
|
case .scanning:
|
||||||
|
return "Scanning for devices..."
|
||||||
|
case .connecting(let device):
|
||||||
|
guard let name = device.name else {
|
||||||
|
return "Connecting to device..."
|
||||||
|
}
|
||||||
|
return "Connecting to \(name)..."
|
||||||
|
case .discoveringServices(let device):
|
||||||
|
guard let name = device.name else {
|
||||||
|
return "Setting up device..."
|
||||||
|
}
|
||||||
|
return "Setting up \(name)..."
|
||||||
|
case .discoveringCharacteristic(let device):
|
||||||
|
guard let name = device.name else {
|
||||||
|
return "Setting up device..."
|
||||||
|
}
|
||||||
|
return "Setting up \(name)..."
|
||||||
|
case .configured(let device, _):
|
||||||
|
guard let name = device.name else {
|
||||||
|
return "Connected"
|
||||||
|
}
|
||||||
|
return "Connected to \(name)"
|
||||||
|
case .disconnected:
|
||||||
|
return "Not connected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceState: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .bluetoothDisabled:
|
||||||
|
return "Bluetooth disabled"
|
||||||
|
case .bluetoothEnabled:
|
||||||
|
return "Bluetooth enabled"
|
||||||
|
case .scanning:
|
||||||
|
return "Searching for device"
|
||||||
|
case .connecting:
|
||||||
|
return "Connecting to device"
|
||||||
|
case .discoveringServices:
|
||||||
|
return "Discovering services"
|
||||||
|
case .discoveringCharacteristic:
|
||||||
|
return "Discovering characteristics"
|
||||||
|
case .configured:
|
||||||
|
return "Connected"
|
||||||
|
case .disconnected:
|
||||||
|
return "Disconnected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DeviceState: Equatable {
|
||||||
|
|
||||||
|
}
|
123
TempTrack/Temperature/TemperatureDataTransfer.swift
Normal file
123
TempTrack/Temperature/TemperatureDataTransfer.swift
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TemperatureDataTransfer {
|
||||||
|
|
||||||
|
private let startDateOfCurrentTransfer: Date
|
||||||
|
|
||||||
|
private let interval: Int
|
||||||
|
|
||||||
|
weak var delegate: TemperatureDataTransferDelegate?
|
||||||
|
|
||||||
|
private var dataBuffer: Data = Data()
|
||||||
|
|
||||||
|
private(set) var currentByteIndex = 0
|
||||||
|
|
||||||
|
private(set) var size: Int
|
||||||
|
|
||||||
|
private(set) var blockSize: Int
|
||||||
|
|
||||||
|
private var numberOfRecordingsInCurrentTransfer = 0
|
||||||
|
|
||||||
|
private(set) var lastRecording: TemperatureMeasurement = .init(sensor0: .notFound, sensor1: .notFound, date: .now)
|
||||||
|
|
||||||
|
private var dateOfNextRecording: Date {
|
||||||
|
startDateOfCurrentTransfer.addingTimeInterval(TimeInterval(numberOfRecordingsInCurrentTransfer * interval))
|
||||||
|
}
|
||||||
|
|
||||||
|
var unprocessedByteCount: Int {
|
||||||
|
dataBuffer.count
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingBytesToTransfer: Int {
|
||||||
|
size - currentByteIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
init(info: DeviceInfo) {
|
||||||
|
self.interval = info.measurementInterval
|
||||||
|
let recordingTime = info.numberOfMeasurements * info.measurementInterval
|
||||||
|
self.startDateOfCurrentTransfer = info.nextMeasurement.addingTimeInterval(-TimeInterval(recordingTime))
|
||||||
|
self.size = info.numberOfRecordedBytes
|
||||||
|
self.blockSize = info.transferBlockSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(info: DeviceInfo) {
|
||||||
|
self.size = info.numberOfRecordedBytes
|
||||||
|
self.blockSize = info.transferBlockSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextRequest() -> BluetoothRequest {
|
||||||
|
guard remainingBytesToTransfer > 0 else {
|
||||||
|
return .clearRecordingBuffer(byteCount: size)
|
||||||
|
}
|
||||||
|
let chunkSize = min(remainingBytesToTransfer, blockSize)
|
||||||
|
return .getRecordingData(offset: currentByteIndex, count: chunkSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(data: Data, offset: Int, count: Int) {
|
||||||
|
dataBuffer.append(data)
|
||||||
|
currentByteIndex += data.count
|
||||||
|
processBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processBytes() {
|
||||||
|
while !dataBuffer.isEmpty {
|
||||||
|
let byte = dataBuffer.removeFirst()
|
||||||
|
guard (byte == 0xFF) else {
|
||||||
|
addRelative(byte: byte)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
guard dataBuffer.count >= 2 else {
|
||||||
|
// Wait for more data
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let temp0 = TemperatureValue(byte: dataBuffer.removeFirst())
|
||||||
|
let temp1 = TemperatureValue(byte: dataBuffer.removeFirst())
|
||||||
|
add(sensor0: temp0, sensor1: temp1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeTransfer() {
|
||||||
|
processBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addRelative(byte: UInt8) {
|
||||||
|
add(sensor0: convertTemp(value: byte >> 4, relativeTo: lastRecording.sensor0),
|
||||||
|
sensor1: convertTemp(value: byte & 0x0F, relativeTo: lastRecording.sensor1))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func add(sensor0: TemperatureValue, sensor1: TemperatureValue) {
|
||||||
|
let measurement = TemperatureMeasurement(
|
||||||
|
sensor0: sensor0,
|
||||||
|
sensor1: sensor1,
|
||||||
|
date: dateOfNextRecording)
|
||||||
|
|
||||||
|
numberOfRecordingsInCurrentTransfer += 1
|
||||||
|
if measurement.sensor0.isValid {
|
||||||
|
lastRecording.sensor0 = measurement.sensor0
|
||||||
|
}
|
||||||
|
if measurement.sensor1.isValid {
|
||||||
|
lastRecording.sensor1 = measurement.sensor1
|
||||||
|
}
|
||||||
|
lastRecording.date = measurement.date
|
||||||
|
delegate?.didReceiveRecording(measurement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertTemp(value: UInt8, relativeTo previous: TemperatureValue) -> TemperatureValue {
|
||||||
|
if value == 0 {
|
||||||
|
return .notFound
|
||||||
|
}
|
||||||
|
let newValue = previous.relativeValue - (Double(value) - 8) * 0.5
|
||||||
|
return .value(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private extension TemperatureValue {
|
||||||
|
|
||||||
|
var relativeValue: Double {
|
||||||
|
if case .value(let double) = self {
|
||||||
|
return double
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol TemperatureDataTransferDelegate: AnyObject {
|
||||||
|
|
||||||
|
func didReceiveRecording(_ measurement: TemperatureMeasurement)
|
||||||
|
}
|
10
TempTrack/Temperature/TemperatureMeasurement.swift
Normal file
10
TempTrack/Temperature/TemperatureMeasurement.swift
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct TemperatureMeasurement {
|
||||||
|
|
||||||
|
var sensor0: TemperatureValue
|
||||||
|
|
||||||
|
var sensor1: TemperatureValue
|
||||||
|
|
||||||
|
var date: Date
|
||||||
|
}
|
94
TempTrack/Temperature/TemperatureSensor.swift
Normal file
94
TempTrack/Temperature/TemperatureSensor.swift
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import Foundation
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct TemperatureSensor {
|
||||||
|
|
||||||
|
let address: [UInt8]
|
||||||
|
|
||||||
|
let value: TemperatureValue
|
||||||
|
|
||||||
|
let date: Date
|
||||||
|
|
||||||
|
var optionalValue: Double? {
|
||||||
|
value.optionalValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var hexAddress: String {
|
||||||
|
String(format: "%02X %02X %02X %02X %02X %02X %02X %02X",
|
||||||
|
address[0], address[1], address[2], address[3],
|
||||||
|
address[4], address[5], address[6], address[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
var temperatureIcon: SFSymbol {
|
||||||
|
TemperatureSensor.temperatureIcon(optionalValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var temperatureText: String {
|
||||||
|
value.text
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateText: String {
|
||||||
|
date.timePassedText
|
||||||
|
}
|
||||||
|
|
||||||
|
static func temperatureIcon(_ temp: Double?) -> SFSymbol {
|
||||||
|
guard let temp else {
|
||||||
|
return .thermometerMediumSlash
|
||||||
|
}
|
||||||
|
guard temp > 0 else {
|
||||||
|
return .thermometerSnowflake
|
||||||
|
}
|
||||||
|
guard temp > 15 else {
|
||||||
|
return .thermometerLow
|
||||||
|
}
|
||||||
|
guard temp > 25 else {
|
||||||
|
return .thermometerMedium
|
||||||
|
}
|
||||||
|
return .thermometerHigh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TemperatureSensor {
|
||||||
|
|
||||||
|
init?(address: [UInt8], valueByte: UInt8, secondsAgo: UInt16) {
|
||||||
|
guard address.contains(where: { $0 != 0 }) else {
|
||||||
|
// Empty address
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.address = address
|
||||||
|
self.value = .init(byte: valueByte)
|
||||||
|
self.date = Date().addingTimeInterval(-TimeInterval(secondsAgo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
|
||||||
|
var timePassedText: String {
|
||||||
|
let secs = Int(-timeIntervalSinceNow.rounded())
|
||||||
|
guard secs > 1 else {
|
||||||
|
return "Now"
|
||||||
|
}
|
||||||
|
guard secs >= 60 else {
|
||||||
|
return "\(secs) seconds ago"
|
||||||
|
}
|
||||||
|
let minutes = secs / 60
|
||||||
|
guard minutes > 1 else {
|
||||||
|
return "1 minute ago"
|
||||||
|
}
|
||||||
|
guard minutes >= 60 else {
|
||||||
|
return "\(minutes) minutes ago"
|
||||||
|
}
|
||||||
|
let hours = minutes / 60
|
||||||
|
guard hours > 1 else {
|
||||||
|
return "1 hour ago"
|
||||||
|
}
|
||||||
|
guard hours >= 60 else {
|
||||||
|
return "\(hours) hours ago"
|
||||||
|
}
|
||||||
|
let days = hours / 24
|
||||||
|
guard days > 1 else {
|
||||||
|
return "1 day ago"
|
||||||
|
}
|
||||||
|
return "\(days) days ago"
|
||||||
|
}
|
||||||
|
}
|
43
TempTrack/Temperature/TemperatureValue.swift
Normal file
43
TempTrack/Temperature/TemperatureValue.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum TemperatureValue {
|
||||||
|
case notFound
|
||||||
|
case invalidMeasurement
|
||||||
|
case value(Double)
|
||||||
|
|
||||||
|
init(byte: UInt8) {
|
||||||
|
switch byte {
|
||||||
|
case 0:
|
||||||
|
self = .notFound
|
||||||
|
case 1:
|
||||||
|
self = .invalidMeasurement
|
||||||
|
default:
|
||||||
|
self = .value(Double(byte) * 0.5 - 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var optionalValue: Double? {
|
||||||
|
if case .value(let val) = self {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var isValid: Bool {
|
||||||
|
if case .value = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var text: String {
|
||||||
|
switch self {
|
||||||
|
case .notFound:
|
||||||
|
return "No sensor"
|
||||||
|
case .invalidMeasurement:
|
||||||
|
return "Invalid"
|
||||||
|
case .value(let double):
|
||||||
|
return "\(Int(double.rounded()))°C"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
TempTrack/TemperatureStorage.swift
Normal file
12
TempTrack/TemperatureStorage.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TemperatureStorage {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TemperatureStorage: TemperatureDataTransferDelegate {
|
||||||
|
|
||||||
|
func didReceiveRecording(_ measurement: TemperatureMeasurement) {
|
||||||
|
//print("Temperature \(measurement.date): \(temp1), \(temp2)")
|
||||||
|
}
|
||||||
|
}
|
166
TempTrack/Views/DeviceInfoView.swift
Normal file
166
TempTrack/Views/DeviceInfoView.swift
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.locale = .current
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .short
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
struct DeviceInfoView: View {
|
||||||
|
|
||||||
|
private let storageWarnBytes = 500
|
||||||
|
|
||||||
|
let info: DeviceInfo
|
||||||
|
|
||||||
|
private var runTimeString: String {
|
||||||
|
let number = info.numberOfSecondsRunning
|
||||||
|
guard number >= 60 else {
|
||||||
|
return "\(number) seconds"
|
||||||
|
}
|
||||||
|
let minutes = number / 60
|
||||||
|
guard minutes > 1 else {
|
||||||
|
return "1 minute"
|
||||||
|
}
|
||||||
|
guard minutes >= 60 else {
|
||||||
|
return "\(minutes) minutes"
|
||||||
|
}
|
||||||
|
let hours = minutes / 60
|
||||||
|
guard hours > 1 else {
|
||||||
|
return "1 hour"
|
||||||
|
}
|
||||||
|
guard hours >= 60 else {
|
||||||
|
return "\(hours) hours"
|
||||||
|
}
|
||||||
|
let days = hours / 24
|
||||||
|
guard days > 1 else {
|
||||||
|
return "1 day"
|
||||||
|
}
|
||||||
|
return "\(days) days"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var nextUpdateText: String {
|
||||||
|
let secs = Int(info.nextMeasurement.timeIntervalSinceNow.rounded())
|
||||||
|
guard secs > 1 else {
|
||||||
|
return "Now"
|
||||||
|
}
|
||||||
|
return "In \(secs) seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var storageIcon: SFSymbol {
|
||||||
|
if info.storageSize - info.numberOfRecordedBytes < storageWarnBytes {
|
||||||
|
return .externaldriveTrianglebadgeExclamationmark
|
||||||
|
}
|
||||||
|
return .externaldrive
|
||||||
|
}
|
||||||
|
|
||||||
|
private var storageText: String {
|
||||||
|
if info.storageSize <= 0 {
|
||||||
|
return "\(info.numberOfRecordedBytes)"
|
||||||
|
}
|
||||||
|
return "\(info.numberOfRecordedBytes) / \(info.storageSize) Bytes (\(info.storageFillPercentage) %)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func sensorView(_ sensor: TemperatureSensor?, id: Int) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text("Sensor \(id)")
|
||||||
|
.font(.headline)
|
||||||
|
if let sensor {
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: sensor.temperatureIcon)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(sensor.temperatureText)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .arrowTriangle2Circlepath)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(sensor.updateText)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .tag)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(sensor.hexAddress)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .thermometerMediumSlash)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text("Not connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text("Recording")
|
||||||
|
.font(.headline)
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .power)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(df.string(from: info.deviceStartTime))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .clock)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text("\(runTimeString)")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .stopwatch)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text("Every \(info.measurementInterval) seconds")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .arrowTriangle2Circlepath)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(nextUpdateText)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text("Storage")
|
||||||
|
.font(.headline)
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .speedometer)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text("\(info.numberOfMeasurements) Measurements")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: storageIcon)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text(storageText)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .iphoneAndArrowForward)
|
||||||
|
.frame(width: 30)
|
||||||
|
Text("\(info.transferBlockSize) Byte Block Size")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sensorView(info.sensor0, id: 0)
|
||||||
|
sensorView(info.sensor1, id: 1)
|
||||||
|
Spacer()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("Updated \(info.receivedDate.timePassedText)")
|
||||||
|
.font(.footnote)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeviceInfoView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
DeviceInfoView(info: .mock)
|
||||||
|
.previewLayout(.fixed(width: 375, height: 500))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user