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