Newer Older
351 lines | 13.999kb
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 }
Bogdan Timofte authored 2 weeks ago
21
    private let actionButtonSpacing: CGFloat = 12
22
    private let actionButtonMaxWidth: CGFloat = 156
23
    private let actionButtonMinWidth: CGFloat = 88
24
    private let actionButtonHeight: CGFloat = 108
Bogdan Timofte authored 2 weeks ago
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 {
Bogdan Timofte authored 2 weeks ago
115
        GeometryReader { proxy in
116
            let buttonWidth = actionButtonWidth(for: proxy.size.width)
117

            
118
            HStack {
119
                Spacer(minLength: 0)
120
                HStack(spacing: actionButtonSpacing) {
121
                    meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal, width: buttonWidth) {
122
                        dataGroupsViewVisibility.toggle()
123
                    }
124
                    .sheet(isPresented: $dataGroupsViewVisibility) {
125
                        DataGroupsView(visibility: $dataGroupsViewVisibility)
126
                            .environmentObject(meter)
127
                    }
Bogdan Timofte authored 2 weeks ago
128

            
Bogdan Timofte authored 2 weeks ago
129
                    if meter.supportsRecordingView {
130
                        meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink, width: buttonWidth) {
131
                            recordingViewVisibility.toggle()
132
                        }
133
                        .sheet(isPresented: $recordingViewVisibility) {
134
                            RecordingView(visibility: $recordingViewVisibility)
135
                                .environmentObject(meter)
136
                        }
137
                    }
Bogdan Timofte authored 2 weeks ago
138

            
Bogdan Timofte authored 2 weeks ago
139
                    meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue, width: buttonWidth) {
140
                        measurementsViewVisibility.toggle()
141
                    }
142
                    .sheet(isPresented: $measurementsViewVisibility) {
143
                        MeasurementsView(visibility: $measurementsViewVisibility)
144
                            .environmentObject(meter.measurements)
145
                    }
146
                }
147
                Spacer(minLength: 0)
Bogdan Timofte authored 2 weeks ago
148
            }
149
        }
Bogdan Timofte authored 2 weeks ago
150
        .frame(height: actionButtonHeight)
Bogdan Timofte authored 2 weeks ago
151
    }
Bogdan Timofte authored 2 weeks ago
152

            
153
    fileprivate func connectionControlButton() -> some View {
Bogdan Timofte authored 2 weeks ago
154
        let connected = meter.operationalState >= .peripheralConnectionPending
155
        let tint = connected ? Color.red : Color.green
156

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

            
Bogdan Timofte authored 2 weeks ago
196
    fileprivate func meterSheetButton(icon: String, title: String, tint: Color, width: CGFloat, action: @escaping () -> Void) -> some View {
Bogdan Timofte authored 2 weeks ago
197
        Button(action: action) {
Bogdan Timofte authored 2 weeks ago
198
            VStack(spacing: 10) {
Bogdan Timofte authored 2 weeks ago
199
                Image(systemName: icon)
Bogdan Timofte authored 2 weeks ago
200
                    .font(.system(size: 20, weight: .semibold))
201
                    .frame(width: 40, height: 40)
202
                    .background(Circle().fill(tint.opacity(0.14)))
Bogdan Timofte authored 2 weeks ago
203
                Text(title)
Bogdan Timofte authored 2 weeks ago
204
                    .font(.footnote.weight(.semibold))
205
                    .multilineTextAlignment(.center)
206
                    .lineLimit(2)
207
                    .minimumScaleFactor(0.9)
Bogdan Timofte authored 2 weeks ago
208
            }
Bogdan Timofte authored 2 weeks ago
209
            .foregroundColor(tint)
Bogdan Timofte authored 2 weeks ago
210
            .frame(width: width, height: actionButtonHeight)
Bogdan Timofte authored 2 weeks ago
211
            .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.22)
212
        }
213
        .buttonStyle(.plain)
214
    }
215

            
Bogdan Timofte authored 2 weeks ago
216
    private var visibleActionButtonCount: CGFloat {
217
        meter.supportsRecordingView ? 3 : 2
218
    }
219

            
220
    private func actionButtonWidth(for availableWidth: CGFloat) -> CGFloat {
221
        let spacingWidth = actionButtonSpacing * max(visibleActionButtonCount - 1, 0)
222
        let fittedWidth = floor((availableWidth - spacingWidth) / visibleActionButtonCount)
223
        return min(actionButtonMaxWidth, max(actionButtonMinWidth, fittedWidth))
224
    }
225

            
Bogdan Timofte authored 2 weeks ago
226
    private var statusText: String {
227
        switch meter.operationalState {
228
        case .notPresent:
229
            return "Missing"
230
        case .peripheralNotConnected:
231
            return "Ready"
232
        case .peripheralConnectionPending:
233
            return "Connecting"
234
        case .peripheralConnected:
235
            return "Linked"
236
        case .peripheralReady:
237
            return "Preparing"
238
        case .comunicating:
239
            return "Syncing"
240
        case .dataIsAvailable:
241
            return "Live"
242
        }
243
    }
244

            
245
    private var statusColor: Color {
246
        Meter.operationalColor(for: meter.operationalState)
Bogdan Timofte authored 2 weeks ago
247
    }
Bogdan Timofte authored 2 weeks ago
248
}
Bogdan Timofte authored 2 weeks ago
249

            
250
private struct MeterInfoView: View {
251
    @EnvironmentObject private var meter: Meter
252

            
253
    var body: some View {
254
        ScrollView {
255
            VStack(spacing: 14) {
256
                MeterInfoCard(title: "Overview", tint: meter.color) {
257
                    MeterInfoRow(label: "Name", value: meter.name)
258
                    MeterInfoRow(label: "Displayed Model", value: meter.deviceModelSummary)
259
                    MeterInfoRow(label: "Advertised Model", value: meter.modelString)
260
                    MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage)
261
                    MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription)
262
                }
263

            
264
                MeterInfoCard(title: "Identifiers", tint: .blue) {
265
                    MeterInfoRow(label: "MAC", value: meter.btSerial.macAddress.description)
266
                    if meter.modelNumber != 0 {
267
                        MeterInfoRow(label: "Model Code", value: "\(meter.modelNumber)")
268
                    }
269
                }
270

            
Bogdan Timofte authored 2 weeks ago
271
                MeterInfoCard(title: "Screen Reporting", tint: .orange) {
272
                    if meter.reportsCurrentScreenIndex {
273
                        MeterInfoRow(label: "Current Screen", value: meter.currentScreenDescription)
274
                        Text("The active screen index is reported by the meter and mapped by the app to a known label.")
275
                            .font(.footnote)
276
                            .foregroundColor(.secondary)
277
                    } else {
278
                        MeterInfoRow(label: "Current Screen", value: "Not Reported")
279
                        Text("The current screen is not reported by the device payload, or we have not yet identified where and how the protocol announces it.")
280
                            .font(.footnote)
281
                            .foregroundColor(.secondary)
282
                    }
283
                }
284

            
Bogdan Timofte authored 2 weeks ago
285
                if meter.operationalState == .dataIsAvailable {
286
                    MeterInfoCard(title: "Live Device Details", tint: .indigo) {
287
                        if !meter.firmwareVersion.isEmpty {
288
                            MeterInfoRow(label: "Firmware", value: meter.firmwareVersion)
289
                        }
290
                        if meter.serialNumber != 0 {
291
                            MeterInfoRow(label: "Serial", value: "\(meter.serialNumber)")
292
                        }
293
                        if meter.bootCount != 0 {
294
                            MeterInfoRow(label: "Boot Count", value: "\(meter.bootCount)")
295
                        }
296
                    }
297
                } else {
298
                    MeterInfoCard(title: "Live Device Details", tint: .secondary) {
299
                        Text("Connect to the meter to load firmware, serial, and boot details.")
300
                            .font(.footnote)
301
                            .foregroundColor(.secondary)
302
                    }
303
                }
304
            }
305
            .padding()
306
        }
307
        .background(
308
            LinearGradient(
309
                colors: [meter.color.opacity(0.14), Color.clear],
310
                startPoint: .topLeading,
311
                endPoint: .bottomTrailing
312
            )
313
            .ignoresSafeArea()
314
        )
315
        .navigationBarTitle("Meter Info")
Bogdan Timofte authored 2 weeks ago
316
        .navigationBarItems(trailing: RSSIView(RSSI: meter.btSerial.RSSI).frame(width: 18, height: 18))
Bogdan Timofte authored 2 weeks ago
317
    }
318
}
319

            
320
private struct MeterInfoCard<Content: View>: View {
321
    let title: String
322
    let tint: Color
323
    @ViewBuilder var content: Content
324

            
325
    var body: some View {
326
        VStack(alignment: .leading, spacing: 12) {
327
            Text(title)
328
                .font(.headline)
329
            content
330
        }
331
        .frame(maxWidth: .infinity, alignment: .leading)
332
        .padding(18)
333
        .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
334
    }
335
}
336

            
337
private struct MeterInfoRow: View {
338
    let label: String
339
    let value: String
340

            
341
    var body: some View {
342
        HStack {
343
            Text(label)
344
            Spacer()
345
            Text(value)
346
                .foregroundColor(.secondary)
347
                .multilineTextAlignment(.trailing)
348
        }
349
        .font(.footnote)
350
    }
351
}