Newer Older
334 lines | 12.97kb
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")
67
        .navigationBarItems(trailing: HStack (spacing: 0) {
68
            if meter.operationalState > .notPresent {
69
                RSSIView(RSSI: meter.btSerial.RSSI)
70
                    .frame(width: 24)
71
                    .padding(.vertical)
72
            }
Bogdan Timofte authored 2 weeks ago
73
            NavigationLink(destination: MeterInfoView().environmentObject(meter)) {
74
                Image(systemName: "info.circle.fill")
75
                    .padding(.vertical)
76
                    .padding(.leading)
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
                    .padding(.leading)
82
            }
83
        })
84
    }
Bogdan Timofte authored 2 weeks ago
85

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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