246 lines
7.6 KiB
Swift
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
|
||
|
}
|
||
|
}
|