Add log view, manual transfer
This commit is contained in:
parent
01a3aac91b
commit
2cb94a12be
@ -38,6 +38,9 @@
|
|||||||
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; };
|
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */; };
|
||||||
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */; };
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */; };
|
||||||
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */; };
|
E253A9242A2B462500EC6B28 /* TemperatureHistoryChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */; };
|
||||||
|
E2A553F92A399F58005204C3 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553F82A399F58005204C3 /* Log.swift */; };
|
||||||
|
E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FA2A39C82D005204C3 /* LogView.swift */; };
|
||||||
|
E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A553FC2A39C86B005204C3 /* LogEntry.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -71,6 +74,9 @@
|
|||||||
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = "<group>"; };
|
88CDE07D2A28AFF400114294 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = "<group>"; };
|
||||||
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
|
E253A9212A2B39B700EC6B28 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureHistoryChart.swift; sourceTree = "<group>"; };
|
E253A9232A2B462500EC6B28 /* TemperatureHistoryChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureHistoryChart.swift; sourceTree = "<group>"; };
|
||||||
|
E2A553F82A399F58005204C3 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.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>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -91,6 +97,8 @@
|
|||||||
children = (
|
children = (
|
||||||
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */,
|
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */,
|
||||||
88CDE0672A2698B400114294 /* TemperatureStorage.swift */,
|
88CDE0672A2698B400114294 /* TemperatureStorage.swift */,
|
||||||
|
E2A553F82A399F58005204C3 /* Log.swift */,
|
||||||
|
E2A553FC2A39C86B005204C3 /* LogEntry.swift */,
|
||||||
);
|
);
|
||||||
path = Storage;
|
path = Storage;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -169,6 +177,7 @@
|
|||||||
88404DD72A2F381B00D30244 /* HistoryList.swift */,
|
88404DD72A2F381B00D30244 /* HistoryList.swift */,
|
||||||
88404DDC2A2F587400D30244 /* HistoryListRow.swift */,
|
88404DDC2A2F587400D30244 /* HistoryListRow.swift */,
|
||||||
88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */,
|
88404DDE2A2F68E100D30244 /* TemperatureDayOverview.swift */,
|
||||||
|
E2A553FA2A39C82D005204C3 /* LogView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -281,6 +290,8 @@
|
|||||||
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
88CDE0762A28AF0900114294 /* TemperatureValue.swift in Sources */,
|
||||||
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */,
|
88404DE32A31F20E00D30244 /* Int+Extensions.swift in Sources */,
|
||||||
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
88CDE07B2A28AF5100114294 /* BluetoothRequest.swift in Sources */,
|
||||||
|
E2A553FD2A39C86B005204C3 /* LogEntry.swift in Sources */,
|
||||||
|
E2A553F92A399F58005204C3 /* Log.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 */,
|
||||||
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
||||||
@ -288,6 +299,7 @@
|
|||||||
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 */,
|
||||||
|
E2A553FB2A39C82D005204C3 /* LogView.swift in Sources */,
|
||||||
88404DD82A2F381B00D30244 /* HistoryList.swift in Sources */,
|
88404DD82A2F381B00D30244 /* HistoryList.swift in Sources */,
|
||||||
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */,
|
88CDE07E2A28AFF400114294 /* DeviceInfoView.swift in Sources */,
|
||||||
88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */,
|
88404DDD2A2F587400D30244 /* HistoryListRow.swift in Sources */,
|
||||||
|
@ -14,8 +14,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "2bcd249b49178247e6b52bac7d67d6e338a40cee",
|
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
|
||||||
"version" : "4.1.0"
|
"version" : "4.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Binary file not shown.
@ -10,6 +10,17 @@ final class BluetoothClient: ObservableObject {
|
|||||||
private let connection = DeviceManager()
|
private let connection = DeviceManager()
|
||||||
|
|
||||||
private let storage: TemperatureStorage
|
private let storage: TemperatureStorage
|
||||||
|
|
||||||
|
var hasInfo: Bool {
|
||||||
|
deviceInfo != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var isConnected: Bool {
|
||||||
|
if case .configured = deviceState {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
init(storage: TemperatureStorage, deviceInfo: DeviceInfo? = nil) {
|
init(storage: TemperatureStorage, deviceInfo: DeviceInfo? = nil) {
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
@ -24,7 +35,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
private(set) var deviceState: DeviceState = .disconnected {
|
private(set) var deviceState: DeviceState = .disconnected {
|
||||||
didSet {
|
didSet {
|
||||||
print("State: \(deviceState)")
|
log.info("State: \(deviceState)")
|
||||||
if case .configured = deviceState {
|
if case .configured = deviceState {
|
||||||
startRegularUpdates()
|
startRegularUpdates()
|
||||||
} else {
|
} else {
|
||||||
@ -37,7 +48,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
private(set) var deviceInfo: DeviceInfo? {
|
private(set) var deviceInfo: DeviceInfo? {
|
||||||
didSet {
|
didSet {
|
||||||
updateDeviceTimeIfNeeded()
|
updateDeviceTimeIfNeeded()
|
||||||
collectRecordedData()
|
// collectRecordedData()
|
||||||
if let deviceInfo, let runningTransfer {
|
if let deviceInfo, let runningTransfer {
|
||||||
runningTransfer.update(info: deviceInfo)
|
runningTransfer.update(info: deviceInfo)
|
||||||
let next = runningTransfer.nextRequest()
|
let next = runningTransfer.nextRequest()
|
||||||
@ -68,7 +79,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
guard dataUpdateTimer == nil else {
|
guard dataUpdateTimer == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("Starting updates")
|
log.info("Starting updates")
|
||||||
dataUpdateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] timer in
|
dataUpdateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] timer in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
@ -87,7 +98,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
dataUpdateTimer.invalidate()
|
dataUpdateTimer.invalidate()
|
||||||
runningRequest = nil
|
runningRequest = nil
|
||||||
self.dataUpdateTimer = nil
|
self.dataUpdateTimer = nil
|
||||||
print("Ending updates")
|
log.info("Ending updates")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Requests
|
// MARK: Requests
|
||||||
@ -102,7 +113,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
let next = openRequests.removeFirst()
|
let next = openRequests.removeFirst()
|
||||||
|
|
||||||
guard connection.send(next.serialized) else {
|
guard connection.send(next.serialized) else {
|
||||||
print("Failed to start request \(next)")
|
log.warning("Failed to start request \(next)")
|
||||||
performNextRequest()
|
performNextRequest()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -115,11 +126,11 @@ final class BluetoothClient: ObservableObject {
|
|||||||
}
|
}
|
||||||
let type = request.byte
|
let type = request.byte
|
||||||
if let runningRequest, runningRequest.byte == type {
|
if let runningRequest, runningRequest.byte == type {
|
||||||
print("Skipping duplicate request \(request)")
|
log.info("Skipping duplicate request \(request)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard !openRequests.contains(where: { $0.byte == type }) else {
|
guard !openRequests.contains(where: { $0.byte == type }) else {
|
||||||
print("Skipping duplicate request \(request)")
|
log.info("Skipping duplicate request \(request)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
openRequests.append(request)
|
openRequests.append(request)
|
||||||
@ -140,7 +151,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
}
|
}
|
||||||
let time = deviceInfo.deviceStartTime.seconds
|
let time = deviceInfo.deviceStartTime.seconds
|
||||||
addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time))
|
addRequest(.setDeviceStartTime(deviceStartTimeSeconds: time))
|
||||||
print("Setting device start time to \(time) s (\(Date().seconds) current)")
|
log.info("Setting device start time to \(time) s (\(Date().seconds) current)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Data transfer
|
// MARK: Data transfer
|
||||||
@ -148,15 +159,15 @@ final class BluetoothClient: ObservableObject {
|
|||||||
@discardableResult
|
@discardableResult
|
||||||
func collectRecordedData() -> Bool {
|
func collectRecordedData() -> Bool {
|
||||||
guard runningTransfer == nil else {
|
guard runningTransfer == nil else {
|
||||||
print("Transfer already running")
|
log.info("Transfer already running")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else {
|
guard !openRequests.contains(where: { if case .getRecordingData = $0 { return true }; return false }) else {
|
||||||
print("Transfer already in scheduled")
|
log.info("Transfer already scheduled")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
guard let info = deviceInfo else {
|
guard let info = deviceInfo else {
|
||||||
print("No device info to start transfer")
|
log.warning("No device info to start transfer")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
guard info.numberOfStoredMeasurements > 0 else {
|
guard info.numberOfStoredMeasurements > 0 else {
|
||||||
@ -166,14 +177,14 @@ final class BluetoothClient: ObservableObject {
|
|||||||
let transfer = TemperatureDataTransfer(info: info)
|
let transfer = TemperatureDataTransfer(info: info)
|
||||||
runningTransfer = transfer
|
runningTransfer = transfer
|
||||||
let next = transfer.nextRequest()
|
let next = transfer.nextRequest()
|
||||||
print("Starting transfer")
|
log.info("Starting transfer")
|
||||||
addRequest(next)
|
addRequest(next)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
print("No running transfer to process device data")
|
log.warning("No running transfer to process device data")
|
||||||
return // TODO: Start new transfer?
|
return // TODO: Start new transfer?
|
||||||
}
|
}
|
||||||
runningTransfer.add(data: data, offset: offset, count: count)
|
runningTransfer.add(data: data, offset: offset, count: count)
|
||||||
@ -183,7 +194,7 @@ final class BluetoothClient: ObservableObject {
|
|||||||
|
|
||||||
private func decode(info: Data) {
|
private func decode(info: Data) {
|
||||||
guard let newInfo = try? DeviceInfo(info: info) else {
|
guard let newInfo = try? DeviceInfo(info: info) else {
|
||||||
print("Failed to decode device info")
|
log.error("Failed to decode device info")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.deviceInfo = newInfo
|
self.deviceInfo = newInfo
|
||||||
@ -197,25 +208,25 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
performNextRequest()
|
performNextRequest()
|
||||||
}
|
}
|
||||||
guard let runningRequest else {
|
guard let runningRequest else {
|
||||||
print("No request active, but \(data) received")
|
log.warning("No request active, but \(data) received")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.runningRequest = nil
|
self.runningRequest = nil
|
||||||
|
|
||||||
guard data.count > 0 else {
|
guard data.count > 0 else {
|
||||||
print("No response data for request \(runningRequest)")
|
log.error("No response data for request \(runningRequest)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let type = BluetoothResponseType(rawValue: data[0]) else {
|
guard let type = BluetoothResponseType(rawValue: data[0]) else {
|
||||||
print("Unknown response \(data[0]) for request \(runningRequest)")
|
log.error("Unknown response \(data[0]) for request \(runningRequest)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch type {
|
switch type {
|
||||||
case .success:
|
case .success:
|
||||||
break
|
break
|
||||||
case .responseInProgress:
|
case .responseInProgress:
|
||||||
print("Device is busy for \(runningRequest)")
|
log.info("Device is busy for \(runningRequest)")
|
||||||
// Retry the request
|
// Retry the request
|
||||||
addRequest(runningRequest)
|
addRequest(runningRequest)
|
||||||
return
|
return
|
||||||
@ -226,7 +237,7 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
addRequest(.getInfo)
|
addRequest(.getInfo)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("Request \(runningRequest) received non-matching responde about number of bytes to delete")
|
log.error("Request \(runningRequest) received non-matching responde about number of bytes to delete")
|
||||||
case .responseTooLarge:
|
case .responseTooLarge:
|
||||||
guard case .getRecordingData = runningRequest else {
|
guard case .getRecordingData = runningRequest else {
|
||||||
// If requesting bytes fails due to the response size,
|
// If requesting bytes fails due to the response size,
|
||||||
@ -234,9 +245,9 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
addRequest(.getInfo)
|
addRequest(.getInfo)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("Unexpectedly exceeded payload size for request \(runningRequest)")
|
log.error("Unexpectedly exceeded payload size for request \(runningRequest)")
|
||||||
default:
|
default:
|
||||||
print("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,
|
||||||
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
// then requesting new info will resolve the mismatch, and the transfer will be resumed
|
||||||
|
|
||||||
@ -255,14 +266,14 @@ extension BluetoothClient: DeviceManagerDelegate {
|
|||||||
didClearDeviceStorage()
|
didClearDeviceStorage()
|
||||||
|
|
||||||
case .setDeviceStartTime:
|
case .setDeviceStartTime:
|
||||||
print("Device time set")
|
log.info("Device time set")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func didClearDeviceStorage() {
|
private func didClearDeviceStorage() {
|
||||||
guard let runningTransfer else {
|
guard let runningTransfer else {
|
||||||
print("No running transfer after clearing device storage")
|
log.warning("No running transfer after clearing device storage")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runningTransfer.completeTransfer()
|
runningTransfer.completeTransfer()
|
||||||
|
@ -31,7 +31,7 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
func connect() -> Bool {
|
func connect() -> Bool {
|
||||||
switch state {
|
switch state {
|
||||||
case .bluetoothDisabled:
|
case .bluetoothDisabled:
|
||||||
print("Can't connect, bluetooth disabled")
|
log.info("Can't connect, bluetooth disabled")
|
||||||
return false
|
return false
|
||||||
case .disconnected, .bluetoothEnabled:
|
case .disconnected, .bluetoothEnabled:
|
||||||
break
|
break
|
||||||
@ -91,7 +91,6 @@ 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) {
|
||||||
//print("Found device '\(peripheral.name ?? "NO_NAME")'")
|
|
||||||
peripheral.delegate = self
|
peripheral.delegate = self
|
||||||
manager.connect(peripheral)
|
manager.connect(peripheral)
|
||||||
manager.stopScan()
|
manager.stopScan()
|
||||||
@ -107,30 +106,30 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
connect()
|
connect()
|
||||||
case .unsupported:
|
case .unsupported:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
print("Bluetooth is not supported")
|
log.info("Bluetooth is not supported")
|
||||||
case .unknown:
|
case .unknown:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
print("Bluetooth state is unknown")
|
log.info("Bluetooth state is unknown")
|
||||||
case .resetting:
|
case .resetting:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
print("Bluetooth is resetting")
|
log.info("Bluetooth is resetting")
|
||||||
case .unauthorized:
|
case .unauthorized:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
print("Bluetooth is not authorized")
|
log.info("Bluetooth is not authorized")
|
||||||
@unknown default:
|
@unknown default:
|
||||||
state = .bluetoothDisabled
|
state = .bluetoothDisabled
|
||||||
print("Unknown state \(central.state)")
|
log.warning("Unknown state \(central.state)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
//print("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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||||
print("Disconnected from " + peripheral.name!)
|
log.info("Disconnected from " + peripheral.name!)
|
||||||
state = .disconnected
|
state = .disconnected
|
||||||
// Attempt to reconnect
|
// Attempt to reconnect
|
||||||
if shouldConnectIfPossible {
|
if shouldConnectIfPossible {
|
||||||
@ -139,9 +138,9 @@ final class DeviceManager: NSObject, CBCentralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||||
print("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 {
|
||||||
print(error)
|
log.warning(error.localizedDescription)
|
||||||
}
|
}
|
||||||
state = manager.isScanning ? .scanning : .disconnected
|
state = manager.isScanning ? .scanning : .disconnected
|
||||||
// Attempt to reconnect
|
// Attempt to reconnect
|
||||||
@ -155,12 +154,12 @@ extension DeviceManager: CBPeripheralDelegate {
|
|||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||||
guard let services = peripheral.services, !services.isEmpty else {
|
guard let services = peripheral.services, !services.isEmpty else {
|
||||||
print("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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let service = services.first(where: { $0.uuid.uuidString == DeviceManager.serviceUUID.uuidString }) else {
|
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})")
|
log.error("Required service not found for '\(peripheral.name ?? "NO_NAME")': \(services.map { $0.uuid.uuidString})")
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -170,18 +169,18 @@ extension DeviceManager: CBPeripheralDelegate {
|
|||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("Failed to discover characteristics: \(error)")
|
log.error("Failed to discover characteristics: \(error)")
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
|
guard let characteristics = service.characteristics, !characteristics.isEmpty else {
|
||||||
print("No characteristics found for device")
|
log.error("No characteristics found for device")
|
||||||
manager.cancelPeripheralConnection(peripheral)
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for characteristic in characteristics {
|
for characteristic in characteristics {
|
||||||
guard characteristic.uuid == DeviceManager.characteristicUUID else {
|
guard characteristic.uuid == DeviceManager.characteristicUUID else {
|
||||||
print("Unused characteristic \(characteristic.uuid.uuidString)")
|
log.warning("Unused characteristic \(characteristic.uuid.uuidString)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
state = .configured(device: peripheral, characteristic: characteristic)
|
state = .configured(device: peripheral, characteristic: characteristic)
|
||||||
@ -191,35 +190,34 @@ extension DeviceManager: CBPeripheralDelegate {
|
|||||||
|
|
||||||
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 {
|
||||||
print("Peripheral failed to write value for \(characteristic.uuid.uuidString): \(error)")
|
log.error("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?) {
|
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("Failed to get RSSI: \(error)")
|
log.warning("Failed to get RSSI: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastRSSI = RSSI.intValue
|
lastRSSI = RSSI.intValue
|
||||||
print("RSSI: \(lastRSSI)")
|
log.info("RSSI: \(lastRSSI)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("Failed to read value update: \(error)")
|
log.error("Failed to read value update: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard case .configured(device: _, characteristic: let storedCharacteristic) = state else {
|
guard case .configured(device: _, characteristic: let storedCharacteristic) = state else {
|
||||||
print("Received data while not properly configured")
|
log.warning("Received data while not properly configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard characteristic.uuid == storedCharacteristic.uuid else {
|
guard characteristic.uuid == storedCharacteristic.uuid else {
|
||||||
print("Read unknown characteristic \(characteristic.uuid.uuidString)")
|
log.warning("Read unknown characteristic \(characteristic.uuid.uuidString)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let data = characteristic.value else {
|
guard let data = characteristic.value else {
|
||||||
print("No data")
|
log.warning("No data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delegate?.deviceManager(didReceive: data)
|
delegate?.deviceManager(didReceive: data)
|
||||||
|
@ -23,13 +23,12 @@ struct ContentView: View {
|
|||||||
@State
|
@State
|
||||||
var showHistory = false
|
var showHistory = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var showLog = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasDeviceInfo: Bool {
|
|
||||||
bluetoothClient.deviceInfo != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var averageTemperature: Double? {
|
var averageTemperature: Double? {
|
||||||
let t1 = bluetoothClient.deviceInfo?.sensor1?.optionalValue
|
let t1 = bluetoothClient.deviceInfo?.sensor1?.optionalValue
|
||||||
@ -110,17 +109,33 @@ struct ContentView: View {
|
|||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
|
Button {
|
||||||
|
self.showLog.toggle()
|
||||||
|
} label: {
|
||||||
|
Image(systemSymbol: .textBubble)
|
||||||
|
.font(.system(size: 30, weight: .light))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
self.showDeviceInfo = true
|
self.showDeviceInfo = true
|
||||||
} label: {
|
} label: {
|
||||||
if hasDeviceInfo {
|
if bluetoothClient.hasInfo {
|
||||||
Image(systemSymbol: .iphone)
|
Image(systemSymbol: .iphone)
|
||||||
.font(.system(size: 30, weight: .regular))
|
.font(.system(size: 30, weight: .light))
|
||||||
}
|
}
|
||||||
Text(bluetoothClient.deviceState.text)
|
Text(bluetoothClient.deviceState.text)
|
||||||
}
|
}
|
||||||
.disabled(!hasDeviceInfo)
|
.disabled(!bluetoothClient.hasInfo)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
bluetoothClient.collectRecordedData()
|
||||||
|
} label: {
|
||||||
|
Image(systemSymbol: .arrowUpArrowDownCircle)
|
||||||
|
.font(.system(size: 30, weight: .light))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}.disabled(!bluetoothClient.isConnected)
|
||||||
|
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
@ -136,6 +151,10 @@ struct ContentView: View {
|
|||||||
HistoryList()
|
HistoryList()
|
||||||
.environmentObject(storage)
|
.environmentObject(storage)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showLog) {
|
||||||
|
LogView()
|
||||||
|
.environmentObject(log)
|
||||||
|
}
|
||||||
.background(backgroundGradient)
|
.background(backgroundGradient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
TempTrack/Storage/Log.swift
Normal file
48
TempTrack/Storage/Log.swift
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
let log = Log()
|
||||||
|
|
||||||
|
final class Log: ObservableObject {
|
||||||
|
|
||||||
|
private let df: DateFormatter
|
||||||
|
|
||||||
|
init() {
|
||||||
|
df = .init()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .medium
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Level: String {
|
||||||
|
case info = "INFO"
|
||||||
|
case warning = "WARN"
|
||||||
|
case error = "ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var logEntries: [LogEntry] = []
|
||||||
|
|
||||||
|
func info(_ message: String) {
|
||||||
|
log(.info, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func warning(_ message: String) {
|
||||||
|
log(.warning, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func error(_ message: String) {
|
||||||
|
log(.error, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func log(_ level: Level, _ message: String) {
|
||||||
|
let entry = LogEntry(level: level, message: message)
|
||||||
|
logEntries.insert(entry, at: 0)
|
||||||
|
print(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Log.Level: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
}
|
33
TempTrack/Storage/LogEntry.swift
Normal file
33
TempTrack/Storage/LogEntry.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct LogEntry: Identifiable {
|
||||||
|
|
||||||
|
let id: TimeInterval
|
||||||
|
|
||||||
|
let date: Date
|
||||||
|
|
||||||
|
let level: Log.Level
|
||||||
|
|
||||||
|
let message: String
|
||||||
|
|
||||||
|
init(date: Date = Date(), level: Log.Level, message: String) {
|
||||||
|
self.id = date.timeIntervalSince1970
|
||||||
|
self.date = date
|
||||||
|
self.level = level
|
||||||
|
self.message = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .medium
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
extension LogEntry: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
"[\(df.string(from: date))][\(level.rawValue)] \(message)"
|
||||||
|
}
|
||||||
|
}
|
@ -57,6 +57,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
|
|
||||||
if lastMeasurements.isEmpty {
|
if lastMeasurements.isEmpty {
|
||||||
loadLastMeasurements()
|
loadLastMeasurements()
|
||||||
|
loadDailyCounts()
|
||||||
} else {
|
} else {
|
||||||
setDailyCounts(from: lastMeasurements)
|
setDailyCounts(from: lastMeasurements)
|
||||||
}
|
}
|
||||||
@ -71,7 +72,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
try fm.createDirectory(at: storageFolder, withIntermediateDirectories: true)
|
try fm.createDirectory(at: storageFolder, withIntermediateDirectories: true)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to create folder: \(error)")
|
log.error("Failed to create folder: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,17 +100,20 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
let dateIndexOfStart = startDate.dateIndex
|
let dateIndexOfStart = startDate.dateIndex
|
||||||
guard todayIndex != dateIndexOfStart else {
|
guard todayIndex != dateIndexOfStart else {
|
||||||
recentMeasurements = todayValues
|
recentMeasurements = todayValues
|
||||||
|
log.info("Loaded \(recentMeasurements.count) recent measurements")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let yesterdayValues = loadMeasurements(for: dateIndexOfStart)
|
let yesterdayValues = loadMeasurements(for: dateIndexOfStart)
|
||||||
.filter { $0.date >= startDate }
|
.filter { $0.date >= startDate }
|
||||||
recentMeasurements = yesterdayValues + todayValues
|
recentMeasurements = yesterdayValues + todayValues
|
||||||
|
log.info("Loaded \(recentMeasurements.count) recent measurements")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) {
|
private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) {
|
||||||
let startDate = Date().addingTimeInterval(-lastValueInterval).seconds
|
let startDate = Date().addingTimeInterval(-lastValueInterval).seconds
|
||||||
let new = recentMeasurements + measurements
|
let new = recentMeasurements + measurements
|
||||||
recentMeasurements = Array(new.drop { $0.id < startDate })
|
recentMeasurements = Array(new.drop { $0.id < startDate })
|
||||||
|
log.info("\(recentMeasurements.count) recent measurements (of \(measurements.count) new entries)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadMeasurements(for date: Date) -> [TemperatureMeasurement] {
|
private func loadMeasurements(for date: Date) -> [TemperatureMeasurement] {
|
||||||
@ -123,30 +127,30 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
private func loadMeasurements(from fileName: String) -> [TemperatureMeasurement] {
|
private func loadMeasurements(from fileName: String) -> [TemperatureMeasurement] {
|
||||||
let fileUrl = fileUrl(for: fileName)
|
let fileUrl = fileUrl(for: fileName)
|
||||||
guard fm.fileExists(atPath: fileUrl.path) else {
|
guard fm.fileExists(atPath: fileUrl.path) else {
|
||||||
print("No measurements for \(fileName)")
|
log.info("No measurements for \(fileName)")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let content = try Data(contentsOf: fileUrl)
|
let content = try Data(contentsOf: fileUrl)
|
||||||
let points: [TemperatureMeasurement] = try BinaryDecoder.decode(from: content)
|
let points: [TemperatureMeasurement] = try BinaryDecoder.decode(from: content)
|
||||||
print("Loaded \(points.count) points for \(fileName)")
|
log.info("Loaded \(points.count) points from \(fileName)")
|
||||||
return points
|
return points
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to read file \(fileName): \(error)")
|
log.error("Failed to read file \(fileName): \(error)")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(_ measurements: [TemperatureMeasurement]) {
|
func add(_ measurements: [TemperatureMeasurement]) {
|
||||||
let lastDate = self.newestMeasurementDate.seconds
|
let lastDate = self.newestMeasurementDate.seconds
|
||||||
let newValues = measurements
|
let newerValues = measurements.filter { $0.id > lastDate }
|
||||||
.filter { $0.id > lastDate }
|
let newValues = newerValues.splitByDate()
|
||||||
.splitByDate()
|
log.info("Adding \(newValues.count) of \(measurements.count) measurements")
|
||||||
|
|
||||||
for (dateIndex, values) in newValues {
|
for (dateIndex, values) in newValues {
|
||||||
let count = saveNew(values, for: dateIndex)
|
let count = saveNew(values, for: dateIndex)
|
||||||
setDailyCount(count, for: dateIndex)
|
setDailyCount(count, for: dateIndex)
|
||||||
print("Day \(dateIndex): \(count) values")
|
//log.info("Day \(dateIndex): \(count) values")
|
||||||
}
|
}
|
||||||
saveDailyCounts()
|
saveDailyCounts()
|
||||||
updateLastMeasurements(measurements)
|
updateLastMeasurements(measurements)
|
||||||
@ -155,7 +159,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
func removeMeasurements(for dateIndex: Int) {
|
func removeMeasurements(for dateIndex: Int) {
|
||||||
let fileUrl = fileUrl(for: dateIndex)
|
let fileUrl = fileUrl(for: dateIndex)
|
||||||
guard fm.fileExists(atPath: fileUrl.path) else {
|
guard fm.fileExists(atPath: fileUrl.path) else {
|
||||||
print("No measurements for \(fileUrl.lastPathComponent)")
|
log.warning("No measurements for \(fileUrl.lastPathComponent)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
@ -163,7 +167,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
dailyMeasurementCounts = dailyMeasurementCounts.filter { $0.dateIndex != dateIndex }
|
dailyMeasurementCounts = dailyMeasurementCounts.filter { $0.dateIndex != dateIndex }
|
||||||
recentMeasurements = recentMeasurements.filter { $0.date.dateIndex != dateIndex }
|
recentMeasurements = recentMeasurements.filter { $0.date.dateIndex != dateIndex }
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to delete \(fileUrl.lastPathComponent): \(error)")
|
log.error("Failed to delete \(fileUrl.lastPathComponent): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +187,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
let data = try BinaryEncoder.encode(measurements.sorted())
|
let data = try BinaryEncoder.encode(measurements.sorted())
|
||||||
try data.write(to: fileUrl)
|
try data.write(to: fileUrl)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to save \(fileName): \(error)")
|
log.error("Failed to save \(fileName): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +223,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
let data = try Data(contentsOf: overviewFileUrl)
|
let data = try Data(contentsOf: overviewFileUrl)
|
||||||
dailyMeasurementCounts = try BinaryDecoder.decode(from: data)
|
dailyMeasurementCounts = try BinaryDecoder.decode(from: data)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to load overview: \(error)")
|
log.error("Failed to load overview: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +232,7 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
let data = try BinaryEncoder.encode(dailyMeasurementCounts)
|
let data = try BinaryEncoder.encode(dailyMeasurementCounts)
|
||||||
try data.write(to: overviewFileUrl)
|
try data.write(to: overviewFileUrl)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to write overview: \(error)")
|
log.error("Failed to write overview: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,9 +246,12 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
|
|
||||||
func recalculateDailyCounts() {
|
func recalculateDailyCounts() {
|
||||||
do {
|
do {
|
||||||
let newValues: [Int: Int] = try fm.contentsOfDirectory(atPath: storageFolder.path)
|
let files = try fm.contentsOfDirectory(atPath: storageFolder.path)
|
||||||
|
let newValues: [Int: Int] = files
|
||||||
.reduce(into: [:]) { counts, fileName in
|
.reduce(into: [:]) { counts, fileName in
|
||||||
guard let dateIndex = Int(fileName) else {
|
let dateString = fileName.replacingOccurrences(of: ".bin", with: "")
|
||||||
|
guard let dateIndex = Int(dateString) else {
|
||||||
|
log.warning("Found file with invalid name \(fileName)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
counts[dateIndex] = loadMeasurements(from: fileName).count
|
counts[dateIndex] = loadMeasurements(from: fileName).count
|
||||||
@ -253,9 +260,10 @@ final class TemperatureStorage: ObservableObject {
|
|||||||
self.dailyMeasurementCounts = newValues
|
self.dailyMeasurementCounts = newValues
|
||||||
.map { .init(dateIndex: $0.key, count: $0.value) }
|
.map { .init(dateIndex: $0.key, count: $0.value) }
|
||||||
.sorted()
|
.sorted()
|
||||||
|
log.info("Daily counts recalculated from \(files.count) files")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to load daily counts: \(error)")
|
log.error("Failed to load daily counts: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ final class TemperatureDataTransfer {
|
|||||||
|
|
||||||
func add(data: Data, offset: Int, count: Int) {
|
func add(data: Data, offset: Int, count: Int) {
|
||||||
guard currentByteIndex == offset else {
|
guard currentByteIndex == offset else {
|
||||||
print("Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)")
|
log.warning("Discarding \(data.count) bytes at offset \(offset), expected \(currentByteIndex)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dataBuffer.append(data)
|
dataBuffer.append(data)
|
||||||
|
35
TempTrack/Views/LogView.swift
Normal file
35
TempTrack/Views/LogView.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LogView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var log: Log
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .medium
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(log.logEntries) { entry in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text(entry.level.description)
|
||||||
|
Spacer()
|
||||||
|
Text(df.string(from: entry.date))
|
||||||
|
}.font(.footnote)
|
||||||
|
Text(entry.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LogView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LogView()
|
||||||
|
.environmentObject(Log())
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,6 @@ struct TemperatureDayOverview: View {
|
|||||||
init(storage: TemperatureStorage, dateIndex: Int) {
|
init(storage: TemperatureStorage, dateIndex: Int) {
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
let points = storage.loadMeasurements(for: dateIndex)
|
let points = storage.loadMeasurements(for: dateIndex)
|
||||||
print("Loaded \(points.count) points for date \(dateIndex)")
|
|
||||||
self.points = points
|
self.points = points
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user