Newer Older
333 lines | 12.958kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  MeterView.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 04/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8
// MARK: Parent frame https://stackoverflow.com/questions/56832865/how-to-access-parents-frame-in-swiftui
9

            
10
import SwiftUI
11
import CoreBluetooth
12

            
13
struct MeterView: View {
14

            
15
    @EnvironmentObject private var meter: Meter
16

            
17
    @State var dataGroupsViewVisibility: Bool = false
Bogdan Timofte authored 2 weeks ago
18
    @State var recordingViewVisibility: Bool = false
Bogdan Timofte authored 2 weeks ago
19
    @State var measurementsViewVisibility: Bool = false
20
    private var myBounds: CGRect { UIScreen.main.bounds }
21

            
Bogdan Timofte authored 2 weeks ago
22
    private let actionColumns = [
23
        GridItem(.adaptive(minimum: 112, maximum: 180), spacing: 12)
24
    ]
25

            
Bogdan Timofte authored 2 weeks ago
26
    var body: some View {
27
        ScrollView {
Bogdan Timofte authored 2 weeks ago
28
            VStack(alignment: .leading, spacing: 16) {
29
                headerCard
30
                connectionControlButton()
Bogdan Timofte authored 2 weeks ago
31

            
Bogdan Timofte authored 2 weeks ago
32
                if meter.operationalState == .dataIsAvailable {
33
                    actionGrid
Bogdan Timofte authored 2 weeks ago
34

            
Bogdan Timofte authored 2 weeks ago
35
                    if meter.measurements.power.context.isValid {
36
                        MeasurementChartView()
37
                            .environmentObject(meter.measurements)
38
                            .frame(minHeight: myBounds.height / 3.4)
39
                            .padding(16)
40
                            .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
Bogdan Timofte authored 2 weeks ago
41
                    }
Bogdan Timofte authored 2 weeks ago
42

            
43
                    ControlView()
44
                        .padding(16)
45
                        .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.20)
46

            
47
                    LiveView()
48
                        .padding(18)
49
                        .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
Bogdan Timofte authored 2 weeks ago
50
                }
51
            }
Bogdan Timofte authored 2 weeks ago
52
            .padding()
Bogdan Timofte authored 2 weeks ago
53
        }
Bogdan Timofte authored 2 weeks ago
54
        .background(
55
            LinearGradient(
56
                colors: [
57
                    meter.color.opacity(0.22),
58
                    Color.secondary.opacity(0.08),
59
                    Color.clear
60
                ],
61
                startPoint: .topLeading,
62
                endPoint: .bottomTrailing
63
            )
64
            .ignoresSafeArea()
65
        )
Bogdan Timofte authored 2 weeks ago
66
        .navigationBarTitle("Meter")
Bogdan Timofte authored 2 weeks ago
67
        .navigationBarItems(trailing: HStack (spacing: 6) {
Bogdan Timofte authored 2 weeks ago
68
            if meter.operationalState > .notPresent {
69
                RSSIView(RSSI: meter.btSerial.RSSI)
Bogdan Timofte authored 2 weeks ago
70
                    .frame(width: 18, height: 18)
71
                    .padding(.leading, 6)
Bogdan Timofte authored 2 weeks ago
72
                    .padding(.vertical)
73
            }
Bogdan Timofte authored 2 weeks ago
74
            NavigationLink(destination: MeterInfoView().environmentObject(meter)) {
75
                Image(systemName: "info.circle.fill")
76
                    .padding(.vertical)
77
            }
Bogdan Timofte authored 2 weeks ago
78
            NavigationLink(destination: MeterSettingsView().environmentObject(meter)) {
Bogdan Timofte authored 2 weeks ago
79
                Image(systemName: "gearshape.fill")
Bogdan Timofte authored 2 weeks ago
80
                    .padding(.vertical)
81
            }
82
        })
83
    }
Bogdan Timofte authored 2 weeks ago
84

            
85
    private var headerCard: some View {
86
        VStack(alignment: .leading, spacing: 14) {
87
            HStack(alignment: .top) {
88
                VStack(alignment: .leading, spacing: 6) {
89
                    Text(meter.name)
90
                        .font(.system(.title2, design: .rounded).weight(.bold))
91
                    Text(meter.deviceModelSummary)
92
                        .font(.subheadline.weight(.semibold))
93
                        .foregroundColor(.secondary)
94
                }
95
                Spacer()
96
                VStack(alignment: .trailing, spacing: 6) {
97
                    Text(statusText)
98
                        .font(.caption.weight(.bold))
99
                        .padding(.horizontal, 12)
100
                        .padding(.vertical, 6)
101
                        .meterCard(tint: statusColor, fillOpacity: 0.24, strokeOpacity: 0.30, cornerRadius: 999)
102
                    if meter.operationalState > .notPresent {
103
                        Text("RSSI \(meter.btSerial.RSSI)")
104
                            .font(.caption)
105
                            .foregroundColor(.secondary)
106
                    }
107
                }
108
            }
109
        }
110
        .padding(20)
111
        .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
112
    }
113

            
114
    private var actionGrid: some View {
115
        LazyVGrid(columns: actionColumns, spacing: 12) {
116
            meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal) {
117
                dataGroupsViewVisibility.toggle()
118
            }
119
            .sheet(isPresented: $dataGroupsViewVisibility) {
120
                DataGroupsView(visibility: $dataGroupsViewVisibility)
121
                    .environmentObject(meter)
122
            }
123

            
124
            if meter.supportsRecordingView {
125
                meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink) {
126
                    recordingViewVisibility.toggle()
127
                }
128
                .sheet(isPresented: $recordingViewVisibility) {
129
                    RecordingView(visibility: $recordingViewVisibility)
130
                        .environmentObject(meter)
131
                }
132
            }
133

            
134
            meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue) {
135
                measurementsViewVisibility.toggle()
136
            }
137
            .sheet(isPresented: $measurementsViewVisibility) {
138
                MeasurementsView(visibility: $measurementsViewVisibility)
139
                    .environmentObject(meter.measurements)
140
            }
141
        }
142
    }
Bogdan Timofte authored 2 weeks ago
143

            
144
    fileprivate func connectionControlButton() -> some View {
Bogdan Timofte authored 2 weeks ago
145
        let connected = meter.operationalState >= .peripheralConnectionPending
146
        let tint = connected ? Color.red : Color.green
147

            
Bogdan Timofte authored 2 weeks ago
148
        return Group {
149
            if meter.operationalState == .notPresent {
Bogdan Timofte authored 2 weeks ago
150
                HStack(spacing: 10) {
151
                    Image(systemName: "exclamationmark.triangle.fill")
152
                        .foregroundColor(.orange)
153
                    Text("Not found at this time.")
154
                        .fontWeight(.semibold)
155
                    Spacer()
156
                }
157
                .padding(16)
158
                .meterCard(tint: .orange, fillOpacity: 0.18, strokeOpacity: 0.24)
Bogdan Timofte authored 2 weeks ago
159
            } else {
Bogdan Timofte authored 2 weeks ago
160
                Button(action: {
Bogdan Timofte authored 2 weeks ago
161
                    if meter.operationalState < .peripheralConnectionPending {
Bogdan Timofte authored 2 weeks ago
162
                        meter.connect()
Bogdan Timofte authored 2 weeks ago
163
                    } else {
Bogdan Timofte authored 2 weeks ago
164
                        meter.disconnect()
165
                    }
166
                }) {
167
                    HStack {
168
                        Image(systemName: connected ? "xmark.circle.fill" : "bolt.horizontal.circle.fill")
169
                        Text(connected ? "Disconnect" : "Connect")
170
                            .fontWeight(.semibold)
171
                        Spacer()
172
                        Text(statusText)
173
                            .font(.footnote.weight(.medium))
174
                            .foregroundColor(.secondary)
Bogdan Timofte authored 2 weeks ago
175
                    }
Bogdan Timofte authored 2 weeks ago
176
                    .padding(.horizontal, 18)
177
                    .padding(.vertical, 16)
178
                    .frame(maxWidth: .infinity)
179
                    .foregroundColor(tint)
180
                    .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
Bogdan Timofte authored 2 weeks ago
181
                }
Bogdan Timofte authored 2 weeks ago
182
                .buttonStyle(.plain)
Bogdan Timofte authored 2 weeks ago
183
            }
184
        }
185
    }
Bogdan Timofte authored 2 weeks ago
186

            
Bogdan Timofte authored 2 weeks ago
187
    fileprivate func meterSheetButton(icon: String, title: String, tint: Color, action: @escaping () -> Void) -> some View {
Bogdan Timofte authored 2 weeks ago
188
        Button(action: action) {
Bogdan Timofte authored 2 weeks ago
189
            VStack(spacing: 10) {
Bogdan Timofte authored 2 weeks ago
190
                Image(systemName: icon)
Bogdan Timofte authored 2 weeks ago
191
                    .font(.system(size: 20, weight: .semibold))
192
                    .frame(width: 40, height: 40)
193
                    .background(Circle().fill(tint.opacity(0.14)))
Bogdan Timofte authored 2 weeks ago
194
                Text(title)
Bogdan Timofte authored 2 weeks ago
195
                    .font(.footnote.weight(.semibold))
196
                    .multilineTextAlignment(.center)
197
                    .lineLimit(2)
198
                    .minimumScaleFactor(0.9)
Bogdan Timofte authored 2 weeks ago
199
            }
Bogdan Timofte authored 2 weeks ago
200
            .foregroundColor(tint)
201
            .frame(maxWidth: .infinity, minHeight: 106)
202
            .padding(.horizontal, 8)
203
            .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.22)
204
        }
205
        .buttonStyle(.plain)
206
    }
207

            
208
    private var statusText: String {
209
        switch meter.operationalState {
210
        case .notPresent:
211
            return "Missing"
212
        case .peripheralNotConnected:
213
            return "Ready"
214
        case .peripheralConnectionPending:
215
            return "Connecting"
216
        case .peripheralConnected:
217
            return "Linked"
218
        case .peripheralReady:
219
            return "Preparing"
220
        case .comunicating:
221
            return "Syncing"
222
        case .dataIsAvailable:
223
            return "Live"
224
        }
225
    }
226

            
227
    private var statusColor: Color {
228
        Meter.operationalColor(for: meter.operationalState)
Bogdan Timofte authored 2 weeks ago
229
    }
Bogdan Timofte authored 2 weeks ago
230
}
Bogdan Timofte authored 2 weeks ago
231

            
232
private struct MeterInfoView: View {
233
    @EnvironmentObject private var meter: Meter
234

            
235
    var body: some View {
236
        ScrollView {
237
            VStack(spacing: 14) {
238
                MeterInfoCard(title: "Overview", tint: meter.color) {
239
                    MeterInfoRow(label: "Name", value: meter.name)
240
                    MeterInfoRow(label: "Displayed Model", value: meter.deviceModelSummary)
241
                    MeterInfoRow(label: "Advertised Model", value: meter.modelString)
242
                    MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage)
243
                    MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription)
244
                }
245

            
246
                MeterInfoCard(title: "Identifiers", tint: .blue) {
247
                    MeterInfoRow(label: "MAC", value: meter.btSerial.macAddress.description)
248
                    if meter.modelNumber != 0 {
249
                        MeterInfoRow(label: "Model Code", value: "\(meter.modelNumber)")
250
                    }
251
                }
252

            
Bogdan Timofte authored 2 weeks ago
253
                MeterInfoCard(title: "Screen Reporting", tint: .orange) {
254
                    if meter.reportsCurrentScreenIndex {
255
                        MeterInfoRow(label: "Current Screen", value: meter.currentScreenDescription)
256
                        Text("The active screen index is reported by the meter and mapped by the app to a known label.")
257
                            .font(.footnote)
258
                            .foregroundColor(.secondary)
259
                    } else {
260
                        MeterInfoRow(label: "Current Screen", value: "Not Reported")
261
                        Text("The current screen is not reported by the device payload, or we have not yet identified where and how the protocol announces it.")
262
                            .font(.footnote)
263
                            .foregroundColor(.secondary)
264
                    }
265
                }
266

            
Bogdan Timofte authored 2 weeks ago
267
                if meter.operationalState == .dataIsAvailable {
268
                    MeterInfoCard(title: "Live Device Details", tint: .indigo) {
269
                        if !meter.firmwareVersion.isEmpty {
270
                            MeterInfoRow(label: "Firmware", value: meter.firmwareVersion)
271
                        }
272
                        if meter.serialNumber != 0 {
273
                            MeterInfoRow(label: "Serial", value: "\(meter.serialNumber)")
274
                        }
275
                        if meter.bootCount != 0 {
276
                            MeterInfoRow(label: "Boot Count", value: "\(meter.bootCount)")
277
                        }
278
                    }
279
                } else {
280
                    MeterInfoCard(title: "Live Device Details", tint: .secondary) {
281
                        Text("Connect to the meter to load firmware, serial, and boot details.")
282
                            .font(.footnote)
283
                            .foregroundColor(.secondary)
284
                    }
285
                }
286
            }
287
            .padding()
288
        }
289
        .background(
290
            LinearGradient(
291
                colors: [meter.color.opacity(0.14), Color.clear],
292
                startPoint: .topLeading,
293
                endPoint: .bottomTrailing
294
            )
295
            .ignoresSafeArea()
296
        )
297
        .navigationBarTitle("Meter Info")
Bogdan Timofte authored 2 weeks ago
298
        .navigationBarItems(trailing: RSSIView(RSSI: meter.btSerial.RSSI).frame(width: 18, height: 18))
Bogdan Timofte authored 2 weeks ago
299
    }
300
}
301

            
302
private struct MeterInfoCard<Content: View>: View {
303
    let title: String
304
    let tint: Color
305
    @ViewBuilder var content: Content
306

            
307
    var body: some View {
308
        VStack(alignment: .leading, spacing: 12) {
309
            Text(title)
310
                .font(.headline)
311
            content
312
        }
313
        .frame(maxWidth: .infinity, alignment: .leading)
314
        .padding(18)
315
        .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
316
    }
317
}
318

            
319
private struct MeterInfoRow: View {
320
    let label: String
321
    let value: String
322

            
323
    var body: some View {
324
        HStack {
325
            Text(label)
326
            Spacer()
327
            Text(value)
328
                .foregroundColor(.secondary)
329
                .multilineTextAlignment(.trailing)
330
        }
331
        .font(.footnote)
332
    }
333
}