Transfer view, change data flow, actors

This commit is contained in:
Christoph Hagen
2023-07-02 17:29:39 +02:00
parent 8b4c4800c9
commit 396571fd30
24 changed files with 1285 additions and 302 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

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