Add log view, manual transfer

This commit is contained in:
Christoph Hagen 2023-06-14 16:16:56 +02:00
parent 01a3aac91b
commit 2cb94a12be
12 changed files with 237 additions and 74 deletions

View File

@ -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 */,

View File

@ -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"
} }
} }
], ],

View File

@ -11,6 +11,17 @@ final class BluetoothClient: ObservableObject {
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
self.deviceInfo = deviceInfo self.deviceInfo = deviceInfo
@ -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()

View File

@ -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)

View File

@ -23,14 +23,13 @@ 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
guard let t0 = bluetoothClient.deviceInfo?.sensor0?.optionalValue else { guard let t0 = bluetoothClient.deviceInfo?.sensor0?.optionalValue else {
@ -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)
} }
} }

View 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
}
}

View 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)"
}
}

View File

@ -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)")
} }
} }

View File

@ -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)

View 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())
}
}

View File

@ -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()
} }