Showing 5 changed files with 148 additions and 48 deletions
+34 -5
USB Meter/Model/Meter.swift
@@ -133,6 +133,10 @@ class Meter : NSObject, ObservableObject, Identifiable {
133 133
         capabilities.supportsDataGroupCommands
134 134
     }
135 135
 
136
+    var supportsRecordingView: Bool {
137
+        capabilities.supportsRecordingView
138
+    }
139
+
136 140
     var supportsUMSettings: Bool {
137 141
         capabilities.supportsScreenSettings
138 142
     }
@@ -180,6 +184,14 @@ class Meter : NSObject, ObservableObject, Identifiable {
180 184
         return String(format: "%02d:%02d", minutes, seconds)
181 185
     }
182 186
 
187
+    var dataGroupsHint: String? {
188
+        capabilities.dataGroupsHint
189
+    }
190
+
191
+    var recordingThresholdHint: String? {
192
+        capabilities.recordingThresholdHint
193
+    }
194
+
183 195
     @Published var btSerial: BluetoothSerial
184 196
     
185 197
     @Published var measurements = Measurements()
@@ -232,13 +244,17 @@ class Meter : NSObject, ObservableObject, Identifiable {
232 244
     @Published var recordedAH: Double = 0
233 245
     @Published var recordedWH: Double = 0
234 246
     @Published var recording: Bool = false
235
-    @Published var recordingTreshold: Double = 0 /* MARK: Seteaza inutil la pornire {
247
+    @Published var recordingTreshold: Double = 0 {
236 248
         didSet {
237
-            if recordingTreshold != oldValue {
238
-                setrecordingTreshold(to: (recordingTreshold*100).uInt8Value)
249
+            guard recordingTreshold != oldValue else { return }
250
+            if isApplyingRecordingThresholdFromDevice {
251
+                return
239 252
             }
253
+            recordingThresholdTimestamp = Date()
254
+            guard recordingThresholdLoadedFromDevice else { return }
255
+            setrecordingTreshold(to: (recordingTreshold * 100).uInt8Value)
240 256
         }
241
-    } */
257
+    }
242 258
     @Published var currentScreen: UInt16 = 0
243 259
     @Published var recordingDuration: UInt32 = 0
244 260
     @Published var loadResistance: Double = 0
@@ -249,6 +265,9 @@ class Meter : NSObject, ObservableObject, Identifiable {
249 265
     @Published var serialNumber: UInt32 = 0
250 266
     @Published var bootCount: UInt32 = 0
251 267
     private var enableAutoConnect: Bool = false
268
+    private var recordingThresholdTimestamp = Date()
269
+    private var recordingThresholdLoadedFromDevice = false
270
+    private var isApplyingRecordingThresholdFromDevice = false
252 271
         
253 272
     init ( model: Model, with serialPort: BluetoothSerial ) {
254 273
         uuid = serialPort.peripheral.identifier
@@ -338,7 +357,17 @@ class Meter : NSObject, ObservableObject, Identifiable {
338 357
         chargerTypeIndex = snapshot.chargerTypeIndex
339 358
         recordedAH = snapshot.recordedAH
340 359
         recordedWH = snapshot.recordedWH
341
-        recordingTreshold = snapshot.recordingThreshold
360
+
361
+        if recordingThresholdTimestamp < dataDumpRequestTimestamp || !recordingThresholdLoadedFromDevice {
362
+            recordingThresholdLoadedFromDevice = true
363
+            if recordingTreshold != snapshot.recordingThreshold {
364
+                isApplyingRecordingThresholdFromDevice = true
365
+                recordingTreshold = snapshot.recordingThreshold
366
+                isApplyingRecordingThresholdFromDevice = false
367
+            }
368
+        } else {
369
+            track("\(name) - Skip updating recordingThreshold (changed after request).")
370
+        }
342 371
         recordingDuration = snapshot.recordingDuration
343 372
         recording = snapshot.recording
344 373
 
+11 -2
USB Meter/Model/MeterCapabilities.swift
@@ -10,12 +10,15 @@ import SwiftUI
10 10
 struct MeterCapabilities {
11 11
     let availableDataGroupIDs: [UInt8]
12 12
     let supportsDataGroupCommands: Bool
13
+    let supportsRecordingView: Bool
13 14
     let supportsScreenSettings: Bool
14 15
     let supportsRecordingThreshold: Bool
15 16
     let supportsFahrenheit: Bool
16 17
     let supportsChargerDetection: Bool
17 18
     let chargerTypeDescriptions: [UInt16: String]
18 19
     let screenDescriptions: [UInt16: String]
20
+    let dataGroupsHint: String?
21
+    let recordingThresholdHint: String?
19 22
 
20 23
     func chargerTypeDescription(for index: UInt16) -> String {
21 24
         guard supportsChargerDetection else { return "-" }
@@ -34,6 +37,7 @@ extension MeterCapabilities {
34 37
     static let umFamily = MeterCapabilities(
35 38
         availableDataGroupIDs: Array(0...9),
36 39
         supportsDataGroupCommands: true,
40
+        supportsRecordingView: true,
37 41
         supportsScreenSettings: true,
38 42
         supportsRecordingThreshold: true,
39 43
         supportsFahrenheit: true,
@@ -55,18 +59,23 @@ extension MeterCapabilities {
55 59
             3: "Cable Impedance",
56 60
             4: "Graphing",
57 61
             5: "System Settings"
58
-        ]
62
+        ],
63
+        dataGroupsHint: "Group 0 is temporary. Groups 1-9 persist across power cycles.",
64
+        recordingThresholdHint: "Recording starts automatically when current rises above this threshold."
59 65
     )
60 66
 
61 67
     static let tc66c = MeterCapabilities(
62 68
         availableDataGroupIDs: [0, 1],
63 69
         supportsDataGroupCommands: false,
70
+        supportsRecordingView: false,
64 71
         supportsScreenSettings: false,
65 72
         supportsRecordingThreshold: false,
66 73
         supportsFahrenheit: false,
67 74
         supportsChargerDetection: false,
68 75
         chargerTypeDescriptions: [:],
69
-        screenDescriptions: [:]
76
+        screenDescriptions: [:],
77
+        dataGroupsHint: nil,
78
+        recordingThresholdHint: nil
70 79
     )
71 80
 }
72 81
 
+13 -3
USB Meter/Views/Meter/Data Groups/DataGroupsView.swift
@@ -16,7 +16,9 @@ struct DataGroupsView: View {
16 16
     
17 17
     var body: some View {
18 18
         GeometryReader { box in
19
+            let tableReservedHeight: CGFloat = usbMeter.dataGroupsHint == nil ? 100 : 140
19 20
             let rowCount = CGFloat(usbMeter.availableDataGroupIDs.count + 1)
21
+            let rowHeight = (box.size.height - tableReservedHeight) / rowCount
20 22
 
21 23
             VStack (spacing: 1) {
22 24
                 HStack {
@@ -30,18 +32,26 @@ struct DataGroupsView: View {
30 32
                 }
31 33
                 .font(.title)
32 34
                 
33
-                Spacer()
35
+                if let hint = usbMeter.dataGroupsHint {
36
+                    Text(hint)
37
+                        .font(.footnote)
38
+                        .foregroundColor(.secondary)
39
+                        .multilineTextAlignment(.center)
40
+                        .padding(.vertical, 8)
41
+                } else {
42
+                    Spacer(minLength: 8)
43
+                }
34 44
                 
35 45
                 HStack (spacing: 1) {
36 46
                     ForEach (["Group", "Ah", "Wh", "Clear"], id: \.self ) { text in
37 47
                         self.THView( text: text, width: (box.size.width-25)/4 )
38 48
                     }
39 49
                 }
40
-                .frame(height: ( box.size.height - 100 ) / rowCount)
50
+                .frame(height: rowHeight)
41 51
                 ForEach(usbMeter.availableDataGroupIDs, id: \.self) { groupId in
42 52
                     DataGroupRowView(id: groupId, width: ((box.size.width-25) / 4), opacity: groupId.isMultiple(of: 2) ? 0.1 : 0.2)
43 53
                 }
44
-                .frame(height: ( box.size.height - 100 ) / rowCount)
54
+                .frame(height: rowHeight)
45 55
             }
46 56
             .padding()
47 57
         }
+33 -18
USB Meter/Views/Meter/MeterView.swift
@@ -15,6 +15,7 @@ struct MeterView: View {
15 15
     @EnvironmentObject private var meter: Meter
16 16
     
17 17
     @State var dataGroupsViewVisibility: Bool = false
18
+    @State var recordingViewVisibility: Bool = false
18 19
     @State var measurementsViewVisibility: Bool = false
19 20
     private var myBounds: CGRect { UIScreen.main.bounds }
20 21
 
@@ -43,27 +44,32 @@ struct MeterView: View {
43 44
             // MARK: Show Data
44 45
             if ( meter.operationalState ==  .dataIsAvailable) {
45 46
                 Text("Model: \(meter.deviceModelSummary)")
46
-                HStack {
47
-                    Button(action: {self.dataGroupsViewVisibility.toggle()}) {
48
-                        VStack {
49
-                            Image(systemName: "map")
50
-                                .sheet(isPresented: self.$dataGroupsViewVisibility) {
51
-                                    DataGroupsView(visibility: self.$dataGroupsViewVisibility)
52
-                                        .environmentObject(self.meter)
53
-                            }
54
-                            Text("Data Groups")
55
-                        }
47
+                HStack(spacing: 24) {
48
+                    meterSheetButton(icon: "map", title: "Data Groups") {
49
+                        dataGroupsViewVisibility.toggle()
50
+                    }
51
+                    .sheet(isPresented: self.$dataGroupsViewVisibility) {
52
+                        DataGroupsView(visibility: self.$dataGroupsViewVisibility)
53
+                            .environmentObject(self.meter)
56 54
                     }
57
-                    Button(action: {self.measurementsViewVisibility.toggle()}) {
58
-                        VStack {
59
-                            Image(systemName: "recordingtape")
60
-                                .sheet(isPresented: self.$measurementsViewVisibility) {
61
-                                    MeasurementsView(visibility: self.$measurementsViewVisibility)
62
-                                        .environmentObject(self.meter.measurements)
63
-                            }
64
-                            Text("History")
55
+
56
+                    if meter.supportsRecordingView {
57
+                        meterSheetButton(icon: "record.circle", title: "Recording") {
58
+                            recordingViewVisibility.toggle()
59
+                        }
60
+                        .sheet(isPresented: self.$recordingViewVisibility) {
61
+                            RecordingView(visibility: self.$recordingViewVisibility)
62
+                                .environmentObject(self.meter)
65 63
                         }
66 64
                     }
65
+
66
+                    meterSheetButton(icon: "recordingtape", title: "History") {
67
+                        measurementsViewVisibility.toggle()
68
+                    }
69
+                    .sheet(isPresented: self.$measurementsViewVisibility) {
70
+                        MeasurementsView(visibility: self.$measurementsViewVisibility)
71
+                            .environmentObject(self.meter.measurements)
72
+                    }
67 73
                 }
68 74
                 if self.meter.measurements.power.context.isValid {
69 75
                     MeasurementChartView()
@@ -120,4 +126,13 @@ struct MeterView: View {
120 126
         }
121 127
         
122 128
     }
129
+
130
+    fileprivate func meterSheetButton(icon: String, title: String, action: @escaping () -> Void) -> some View {
131
+        Button(action: action) {
132
+            VStack {
133
+                Image(systemName: icon)
134
+                Text(title)
135
+            }
136
+        }
137
+    }
123 138
 }
+57 -20
USB Meter/Views/Meter/RecordingView.swift
@@ -10,41 +10,78 @@ import SwiftUI
10 10
 
11 11
 struct RecordingView: View {
12 12
     
13
+    @Binding var visibility: Bool
13 14
     @EnvironmentObject private var usbMeter: Meter
14 15
     
15 16
     var body: some View {
16
-        VStack {
17
-            Text ("Recorded Data")
18
-            Text ("REC")
19
-                .foregroundColor(usbMeter.recording ? .red : .green)
20
-            HStack {
21
-                VStack {
22
-                    Text ("Capacity")
23
-                    Text ("Energy")
24
-                    Text ("Duration")
25
-                    Text ("Threshold")
26
-                }
27
-                VStack {
28
-                    Text("\(usbMeter.recordedAH.format(decimalDigits: 3)) Ah")
29
-                    Text("\(usbMeter.recordedWH.format(decimalDigits: 3)) Wh")
30
-                    Text(usbMeter.recordingDurationDescription)
17
+        NavigationView {
18
+            ScrollView {
19
+                VStack(spacing: 16) {
20
+                    VStack(spacing: 6) {
21
+                        Text("Device Recording")
22
+                            .font(.headline)
23
+                        Text(usbMeter.recording ? "Active" : "Idle")
24
+                            .foregroundColor(usbMeter.recording ? .red : .secondary)
25
+                    }
26
+                    .frame(maxWidth: .infinity)
27
+                    .padding()
28
+                    .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
29
+
30
+                    HStack(alignment: .top) {
31
+                        VStack(alignment: .leading, spacing: 10) {
32
+                            Text("Capacity")
33
+                            Text("Energy")
34
+                            Text("Duration")
35
+                            Text("Start Threshold")
36
+                        }
37
+                        Spacer()
38
+                        VStack(alignment: .trailing, spacing: 10) {
39
+                            Text("\(usbMeter.recordedAH.format(decimalDigits: 3)) Ah")
40
+                            Text("\(usbMeter.recordedWH.format(decimalDigits: 3)) Wh")
41
+                            Text(usbMeter.recordingDurationDescription)
42
+                            if usbMeter.supportsRecordingThreshold {
43
+                                Text("\(usbMeter.recordingTreshold.format(decimalDigits: 2)) A")
44
+                            } else {
45
+                                Text("Read-only")
46
+                            }
47
+                        }
48
+                    }
49
+                    .padding()
50
+                    .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
51
+
31 52
                     if usbMeter.supportsRecordingThreshold {
32
-                        HStack {
53
+                        VStack(alignment: .leading, spacing: 10) {
54
+                            Text("Start Threshold")
55
+                                .fontWeight(.semibold)
33 56
                             Slider(value: $usbMeter.recordingTreshold, in: 0...0.30, step: 0.01)
34
-                            Text("\(usbMeter.recordingTreshold.format(decimalDigits: 2)) A")
57
+                            if let hint = usbMeter.recordingThresholdHint {
58
+                                Text(hint)
59
+                                    .font(.footnote)
60
+                                    .foregroundColor(.secondary)
61
+                            }
35 62
                         }
36 63
                         .padding()
64
+                        .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
37 65
                     } else {
38
-                        Text("N/A")
66
+                        Text("This model reports recording totals, but the app does not expose remote threshold control for it.")
67
+                            .font(.footnote)
68
+                            .foregroundColor(.secondary)
69
+                            .multilineTextAlignment(.center)
70
+                            .padding()
71
+                            .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
39 72
                     }
40
-                }.padding()
73
+                }
74
+                .padding()
41 75
             }
76
+            .navigationBarTitle("Device Recording", displayMode: .inline)
77
+            .navigationBarItems(trailing: Button("Done") { visibility.toggle() })
42 78
         }
79
+        .navigationViewStyle(StackNavigationViewStyle())
43 80
     }
44 81
 }
45 82
 
46 83
 struct RecordingView_Previews: PreviewProvider {
47 84
     static var previews: some View {
48
-        RecordingView()
85
+        RecordingView(visibility: .constant(true))
49 86
     }
50 87
 }