Transfer view, change data flow, actors
This commit is contained in:
@ -12,7 +12,7 @@ struct DayView: View {
|
||||
let dateIndex: Int
|
||||
|
||||
@EnvironmentObject
|
||||
var storage: TemperatureStorage
|
||||
var storage: PersistentStorage
|
||||
|
||||
var entries: [TemperatureMeasurement] {
|
||||
storage.loadMeasurements(for: dateIndex)
|
||||
@ -33,6 +33,6 @@ struct DayView: View {
|
||||
struct DayView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DayView(dateIndex: Date.now.dateIndex)
|
||||
.environmentObject(TemperatureStorage.mock)
|
||||
.environmentObject(PersistentStorage.mock)
|
||||
}
|
||||
}
|
||||
|
@ -10,16 +10,16 @@ private let df: DateFormatter = {
|
||||
}()
|
||||
|
||||
struct DeviceInfoView: View {
|
||||
|
||||
|
||||
private let storageWarnBytes = 500
|
||||
|
||||
let info: DeviceInfo
|
||||
|
||||
@Binding
|
||||
var isPresented: Bool
|
||||
|
||||
|
||||
private var runTimeString: String {
|
||||
let number = info.numberOfSecondsRunning
|
||||
let number = info.time.secondsSincePowerOn
|
||||
guard number >= 60 else {
|
||||
return "\(number) s"
|
||||
}
|
||||
@ -45,7 +45,7 @@ struct DeviceInfoView: View {
|
||||
}
|
||||
|
||||
private var nextUpdateText: String {
|
||||
let secs = Int(info.nextMeasurement.timeIntervalSinceNow.rounded())
|
||||
let secs = info.time.nextMeasurement.secondsToNow
|
||||
guard secs > 1 else {
|
||||
return "Now"
|
||||
}
|
||||
@ -71,42 +71,25 @@ struct DeviceInfoView: View {
|
||||
Text("Sensor \(id)")
|
||||
.font(.headline)
|
||||
if let sensor {
|
||||
HStack {
|
||||
Image(systemSymbol: sensor.temperatureIcon)
|
||||
.frame(width: 30)
|
||||
Text("\(sensor.temperatureText) (\(sensor.updateText))")
|
||||
}
|
||||
HStack {
|
||||
Image(systemSymbol: .tag)
|
||||
.frame(width: 30)
|
||||
Text(sensor.hexAddress)
|
||||
}
|
||||
IconAndTextView(
|
||||
icon: sensor.temperatureIcon,
|
||||
text: "\(sensor.temperatureText) (\(sensor.updateText))")
|
||||
IconAndTextView(
|
||||
icon: .tag,
|
||||
text: sensor.hexAddress)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemSymbol: .thermometerMediumSlash)
|
||||
.frame(width: 30)
|
||||
Text("Not connected")
|
||||
}
|
||||
IconAndTextView(
|
||||
icon: .thermometerMediumSlash,
|
||||
text: "Not connected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updateText: String {
|
||||
guard info.receivedDate.secondsToNow > 3 else {
|
||||
guard info.time.date.secondsToNow > 3 else {
|
||||
return "Updated Now"
|
||||
}
|
||||
return "Updated \(info.receivedDate.timePassedText)"
|
||||
}
|
||||
|
||||
var clockOffsetText: String {
|
||||
guard info.hasDeviceStartTimeSet else {
|
||||
return "Clock not synchronized"
|
||||
}
|
||||
let offset = info.clockOffset.roundedInt
|
||||
guard abs(offset) > 1 else {
|
||||
return "No clock offset"
|
||||
}
|
||||
return "Offset: \(offset) s"
|
||||
return "Updated \(info.time.date.timePassedText)"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -115,52 +98,32 @@ struct DeviceInfoView: View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("System")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
Image(systemSymbol: .power)
|
||||
.frame(width: 30)
|
||||
Text("\(df.string(from: info.deviceStartTime)) (\(runTimeString))")
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Image(systemSymbol: .clockBadgeExclamationmark)
|
||||
.frame(width: 30)
|
||||
Text(clockOffsetText)
|
||||
}
|
||||
HStack {
|
||||
Image(systemSymbol: .autostartstop)
|
||||
.frame(width: 30)
|
||||
Text("Wakeup: \(info.wakeupReason.text)")
|
||||
Spacer()
|
||||
}
|
||||
IconAndTextView(
|
||||
icon: .power,
|
||||
text: "\(df.string(from: info.time.deviceStartTime)) (\(runTimeString))")
|
||||
IconAndTextView(
|
||||
icon: .autostartstop,
|
||||
text: "Wakeup: \(info.wakeupReason.text)")
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Recording")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
Image(systemSymbol: .stopwatch)
|
||||
.frame(width: 30)
|
||||
Text("\(nextUpdateText) (Every \(info.measurementInterval) s)")
|
||||
Spacer()
|
||||
}
|
||||
IconAndTextView(
|
||||
icon: .stopwatch,
|
||||
text: "\(nextUpdateText) (Every \(info.measurementInterval) s)")
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Storage")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
Image(systemSymbol: .speedometer)
|
||||
.frame(width: 30)
|
||||
Text("\(info.numberOfStoredMeasurements) Measurements (\(info.totalNumberOfMeasurements) total)")
|
||||
}
|
||||
HStack {
|
||||
Image(systemSymbol: storageIcon)
|
||||
.frame(width: 30)
|
||||
Text(storageText)
|
||||
}
|
||||
HStack {
|
||||
Image(systemSymbol: .iphoneAndArrowForward)
|
||||
.frame(width: 30)
|
||||
Text("\(info.transferBlockSize) Byte Block Size")
|
||||
}
|
||||
IconAndTextView(
|
||||
icon: .speedometer,
|
||||
text: "\(info.numberOfStoredMeasurements) Measurements (\(info.time.totalNumberOfMeasurements) total)")
|
||||
IconAndTextView(
|
||||
icon: storageIcon,
|
||||
text: storageText)
|
||||
IconAndTextView(
|
||||
icon: .iphoneAndArrowForward,
|
||||
text: "\(info.transferBlockSize) Byte Block Size")
|
||||
}
|
||||
sensorView(info.sensor0, id: 0)
|
||||
sensorView(info.sensor1, id: 1)
|
||||
|
@ -3,7 +3,7 @@ import SwiftUI
|
||||
struct HistoryList: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var storage: TemperatureStorage
|
||||
var storage: PersistentStorage
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@ -36,6 +36,6 @@ struct HistoryList: View {
|
||||
struct HistoryList_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryList()
|
||||
.environmentObject(TemperatureStorage(lastMeasurements: TemperatureMeasurement.mockData))
|
||||
.environmentObject(PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData))
|
||||
}
|
||||
}
|
||||
|
24
TempTrack/Views/IconAndTextView.swift
Normal file
24
TempTrack/Views/IconAndTextView.swift
Normal file
@ -0,0 +1,24 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct IconAndTextView: View {
|
||||
|
||||
let icon: SFSymbol
|
||||
|
||||
let text: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemSymbol: icon)
|
||||
.frame(width: 30)
|
||||
Text(text)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IconAndTextView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IconAndTextView(icon: .power, text: "Awake time")
|
||||
}
|
||||
}
|
@ -13,17 +13,20 @@ struct LogView: View {
|
||||
var log: Log
|
||||
|
||||
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)
|
||||
NavigationView {
|
||||
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)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Log")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ struct TemperatureDayOverview: View {
|
||||
self.points = points
|
||||
}
|
||||
|
||||
init(storage: TemperatureStorage, dateIndex: Int) {
|
||||
init(storage: PersistentStorage, dateIndex: Int) {
|
||||
let points = storage.loadMeasurements(for: dateIndex)
|
||||
self.points = points
|
||||
update()
|
||||
@ -77,7 +77,7 @@ struct TemperatureDayOverview: View {
|
||||
|
||||
struct TemperatureDayOverview_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TemperatureDayOverview(storage: TemperatureStorage.mock, dateIndex: Date().dateIndex)
|
||||
TemperatureDayOverview(storage: PersistentStorage.mock, dateIndex: Date().dateIndex)
|
||||
.previewLayout(.fixed(width: 350, height: 150))
|
||||
//.background(.gray)
|
||||
}
|
||||
|
245
TempTrack/Views/TransferView.swift
Normal file
245
TempTrack/Views/TransferView.swift
Normal file
@ -0,0 +1,245 @@
|
||||
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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user