import SwiftUI import SFSafeSymbols struct TransferView: View { private let storageWarnBytes = 500 let bluetoothClient: BluetoothDevice @EnvironmentObject var storage: PersistentStorage @State var bytesTransferred: Double = 0.0 @State var totalBytes: Double = 0.0 @State var measurements: [TemperatureMeasurement] = [] @State var transferIsRunning = false private var storageIcon: SFSymbol { guard let info = bluetoothClient.lastDeviceInfo else { return .externaldrive } if info.storageSize - info.numberOfRecordedBytes < storageWarnBytes { return .externaldriveTrianglebadgeExclamationmark } return .externaldrive } private var measurementsText: String { guard let info = bluetoothClient.lastDeviceInfo else { return "No measurements" } return "\(info.numberOfStoredMeasurements) measurements (\(info.time.totalNumberOfMeasurements) total)" } private var storageText: String { guard let info = bluetoothClient.lastDeviceInfo else { return "No data" } if info.storageSize <= 0 { return "\(info.numberOfRecordedBytes) Bytes" } return "\(info.numberOfRecordedBytes) / \(info.storageSize) Bytes (\(info.storageFillPercentage) %)" } private var transferSizeText: String { guard let info = bluetoothClient.lastDeviceInfo else { return "No transfer size" } return "\(info.transferBlockSize) Byte Block Size" } private var transferByteText: String { let total = Int(totalBytes) guard total > 0 else { return "No data" } return "\(Int(bytesTransferred)) / \(total) Bytes" } private var transferMeasurementText: String { guard !measurements.isEmpty else { return "No measurements" } return "\(measurements.count) measurements" } var body: some View { NavigationView { VStack { VStack(alignment: .leading, spacing: 5) { Text("Storage") .font(.headline) IconAndTextView( icon: .speedometer, text: measurementsText) IconAndTextView( icon: storageIcon, text: storageText) IconAndTextView( icon: .iphoneAndArrowForward, text: transferSizeText) } Button(action: clearStorage) { Text("Remove recorded data") } .disabled(transferIsRunning) .padding() VStack(alignment: .leading, spacing: 5) { Text("Transfer") .font(.headline) ProgressView(value: bytesTransferred, total: totalBytes) .progressViewStyle(.linear) .padding(.vertical, 5) IconAndTextView( icon: .externaldrive, text: transferByteText) IconAndTextView( icon: .speedometer, text: transferMeasurementText) } HStack { Button(action: transferData) { Text("Transfer") } .disabled(transferIsRunning) .padding() Spacer() Button(action: saveTransfer) { Text("Save") } .disabled(transferIsRunning || measurements.isEmpty) .padding() Spacer() Button(action: discardTransfer) { Text("Discard") } .disabled(transferIsRunning || measurements.isEmpty) .padding() } Spacer() VStack { } } .padding() .navigationTitle("Data Transfer") .navigationBarTitleDisplayMode(.large) } } func transferData() { guard let info = bluetoothClient.lastDeviceInfo else { return } transferIsRunning = true let total = info.numberOfRecordedBytes let chunkSize = info.transferBlockSize bytesTransferred = 0 totalBytes = Double(total) Task { defer { DispatchQueue.main.async { self.transferIsRunning = false } } var data = Data(capacity: total) while data.count < total { let remainingBytes = total - data.count let currentChunkSize = min(remainingBytes, chunkSize) guard let chunk = await bluetoothClient.getDeviceData(offset: data.count, count: currentChunkSize) else { log.warning("Failed to finish transfer") return } data.append(chunk) DispatchQueue.main.async { self.bytesTransferred = Double(data.count) } } DispatchQueue.main.async { self.bytesTransferred = totalBytes } var measurementCount = 0 let recordingStart = info.currentMeasurementStartTime while !data.isEmpty { let byte = data.removeFirst() guard (byte == 0xFF) else { log.error("Expected 0xFF at index \(total - data.count - 1)") break } guard data.count >= 2 else { log.error("Expected two more bytes after index \(total - data.count - 1)") break } let temp0 = TemperatureValue(byte: data.removeFirst()) let temp1 = TemperatureValue(byte: data.removeFirst()) let date = recordingStart .addingTimeInterval(TimeInterval(measurementCount * info.measurementInterval)) let measurement = TemperatureMeasurement( sensor0: temp0, sensor1: temp1, date: date) measurementCount += 1 DispatchQueue.main.async { self.measurements.append(measurement) } } } } func discardTransfer() { self.measurements = [] self.bytesTransferred = 0 self.totalBytes = 0 } func saveTransfer() { // TODO: Save discardTransfer() } func clearStorage() { guard let byteCount = bluetoothClient.lastDeviceInfo?.numberOfRecordedBytes else { return } Task { guard await bluetoothClient.deleteDeviceData(byteCount: byteCount) else { log.warning("Failed to delete data") return } log.warning("Device storage cleared") } } } struct TransferView_Previews: PreviewProvider { static var previews: some View { let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData) TransferView(bluetoothClient: .init()) .environmentObject(storage) } } private extension TemperatureValue { var relativeValue: Double { if case .value(let double) = self { return double } return 0 } }