TempTrack-iOS/TempTrack/Views/TransferView.swift
2023-07-02 17:29:39 +02:00

246 lines
7.6 KiB
Swift

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