Newer Older
1163 lines | 41.715kb
Bogdan Timofte authored 2 months ago
1
//
2
//  File.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 03/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8
//MARK: Store and documentation: https://www.aliexpress.com/item/32968303350.html
9
//MARK: Protocol: https://sigrok.org/wiki/RDTech_UM_series
10
//MARK: Pithon Code: https://github.com/rfinnie/rdserialtool
11
//MARK: HM-10 Code: https://github.com/hoiberg/HM10-BluetoothSerial-iOS
12
//MARK: Package dependency https://github.com/krzyzanowskim/CryptoSwift
13

            
14
import CoreBluetooth
15
import SwiftUI
16

            
17
/**
18
 Supprted USB Meters
19
 # UM25C
20
 # TC66
21
 * Reverse Engineering
22
 [UM Series](https://sigrok.org/wiki/RDTech_UM_series)
23
 [TC66C](https://sigrok.org/wiki/RDTech_TC66C)
24
 */
Bogdan Timofte authored a month ago
25
enum Model: CaseIterable, Hashable {
Bogdan Timofte authored 2 months ago
26
    case UM25C
27
    case UM34C
28
    case TC66C
29
}
30

            
Bogdan Timofte authored 2 months ago
31
enum TemperatureUnitPreference: String, CaseIterable, Identifiable {
32
    case celsius
33
    case fahrenheit
34

            
35
    var id: String { rawValue }
36

            
37
    var title: String {
38
        switch self {
39
        case .celsius:
40
            return "Celsius"
41
        case .fahrenheit:
42
            return "Fahrenheit"
43
        }
44
    }
45

            
46
    var symbol: String {
47
        switch self {
48
        case .celsius:
49
            return "℃"
50
        case .fahrenheit:
51
            return "℉"
52
        }
53
    }
54
}
55

            
Bogdan Timofte authored 2 months ago
56
private extension TemperatureUnitPreference {
57
    var localeTitle: String {
58
        switch self {
59
        case .celsius:
60
            return "System (Celsius)"
61
        case .fahrenheit:
62
            return "System (Fahrenheit)"
63
        }
64
    }
65
}
66

            
Bogdan Timofte authored 2 months ago
67
enum ChargeRecordState {
68
    case waitingForStart
69
    case active
70
    case completed
71
}
72

            
Bogdan Timofte authored 2 months ago
73
class Meter : NSObject, ObservableObject, Identifiable {
74

            
Bogdan Timofte authored 2 months ago
75
    private static func shouldLogOperationalStateTransition(from oldValue: OperationalState, to newValue: OperationalState) -> Bool {
76
        switch (oldValue, newValue) {
77
        case (.comunicating, .dataIsAvailable), (.dataIsAvailable, .comunicating):
78
            return false
79
        default:
80
            return true
81
        }
82
    }
83

            
Bogdan Timofte authored 2 months ago
84
    enum OperationalState: Int, Comparable {
85
        case notPresent
86
        case peripheralNotConnected
87
        case peripheralConnectionPending
88
        case peripheralConnected
89
        case peripheralReady
90
        case comunicating
91
        case dataIsAvailable
92

            
93
        static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
94
            return lhs.rawValue < rhs.rawValue
95
        }
96
    }
97

            
98
    @Published var operationalState = OperationalState.peripheralNotConnected {
99
        didSet {
Bogdan Timofte authored 2 months ago
100
            guard operationalState != oldValue else { return }
Bogdan Timofte authored 2 months ago
101
            if Self.shouldLogOperationalStateTransition(from: oldValue, to: operationalState) {
102
                track("\(name) - Operational state changed from \(oldValue) to \(operationalState)")
103
            }
Bogdan Timofte authored 2 months ago
104
            switch operationalState {
105
            case .notPresent:
Bogdan Timofte authored 2 months ago
106
                cancelPendingDataDumpRequest(reason: "meter missing")
Bogdan Timofte authored 2 months ago
107
                break
108
            case .peripheralNotConnected:
Bogdan Timofte authored 2 months ago
109
                cancelPendingDataDumpRequest(reason: "peripheral disconnected")
Bogdan Timofte authored 2 months ago
110
                handleMeasurementDiscontinuity(at: Date())
Bogdan Timofte authored 2 months ago
111
                if !commandQueue.isEmpty {
112
                    track("\(name) - Clearing \(commandQueue.count) queued commands after disconnect")
113
                    commandQueue.removeAll()
114
                }
Bogdan Timofte authored 2 months ago
115
                if enableAutoConnect {
116
                    track("\(name) - Reconnecting...")
117
                    btSerial.connect()
118
                }
119
            case .peripheralConnectionPending:
Bogdan Timofte authored 2 months ago
120
                cancelPendingDataDumpRequest(reason: "connection pending")
Bogdan Timofte authored 2 months ago
121
                break
122
            case .peripheralConnected:
Bogdan Timofte authored 2 months ago
123
                cancelPendingDataDumpRequest(reason: "services not ready yet")
Bogdan Timofte authored 2 months ago
124
                break
125
            case .peripheralReady:
Bogdan Timofte authored 2 months ago
126
                scheduleDataDumpRequest(after: 0.5, reason: "peripheral ready")
Bogdan Timofte authored 2 months ago
127
            case .comunicating:
128
                break
129
            case .dataIsAvailable:
130
                break
131
            }
132
        }
133
    }
134

            
135
    static func operationalColor(for state: OperationalState) -> Color {
136
        switch state {
137
        case .notPresent:
138
            return .red
139
        case .peripheralNotConnected:
140
            return .blue
141
        case .peripheralConnectionPending:
142
            return .yellow
143
        case .peripheralConnected:
144
            return .yellow
145
        case .peripheralReady:
146
            return .orange
147
        case .comunicating:
148
            return .orange
149
        case .dataIsAvailable:
150
            return .green
151
        }
152
    }
153

            
154
    private var wdTimer: Timer?
155

            
Bogdan Timofte authored 2 months ago
156
    @Published var lastSeen: Date? {
Bogdan Timofte authored 2 months ago
157
        didSet {
158
            wdTimer?.invalidate()
Bogdan Timofte authored 2 months ago
159
            guard lastSeen != nil else { return }
160
            appData.noteMeterSeen(at: lastSeen!, macAddress: btSerial.macAddress.description)
Bogdan Timofte authored 2 months ago
161
            if operationalState == .peripheralNotConnected {
162
                wdTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false, block: {_ in
163
                    track("\(self.name) - Lost advertisments...")
164
                    self.operationalState = .notPresent
165
                })
166
            } else if operationalState == .notPresent {
167
               operationalState = .peripheralNotConnected
168
            }
169
        }
170
    }
171

            
172
    var uuid: UUID
173
    var model: Model
174
    var modelString: String
175

            
Bogdan Timofte authored 2 months ago
176
    private var isSyncingNameFromStore = false
177

            
178
    @Published var name: String {
Bogdan Timofte authored 2 months ago
179
        didSet {
Bogdan Timofte authored 2 months ago
180
            guard !isSyncingNameFromStore else { return }
181
            guard oldValue != name else { return }
182
            appData.setMeterName(name, for: btSerial.macAddress.description)
Bogdan Timofte authored 2 months ago
183
        }
184
    }
Bogdan Timofte authored 2 months ago
185

            
Bogdan Timofte authored 2 months ago
186
    var preferredTabIdentifier: String = "home"
187

            
Bogdan Timofte authored 2 months ago
188
    @Published private(set) var lastConnectedAt: Date?
Bogdan Timofte authored 2 months ago
189

            
190
    var color : Color {
191
        get {
Bogdan Timofte authored 2 months ago
192
            return model.color
Bogdan Timofte authored 2 months ago
193
        }
194
    }
195

            
Bogdan Timofte authored 2 months ago
196
    var capabilities: MeterCapabilities {
197
        model.capabilities
198
    }
199

            
Bogdan Timofte authored 2 months ago
200
    var availableDataGroupIDs: [UInt8] {
Bogdan Timofte authored 2 months ago
201
        capabilities.availableDataGroupIDs
Bogdan Timofte authored 2 months ago
202
    }
203

            
204
    var supportsDataGroupCommands: Bool {
Bogdan Timofte authored 2 months ago
205
        capabilities.supportsDataGroupCommands
Bogdan Timofte authored 2 months ago
206
    }
207

            
Bogdan Timofte authored 2 months ago
208
    var supportsRecordingView: Bool {
209
        capabilities.supportsRecordingView
210
    }
211

            
Bogdan Timofte authored 2 months ago
212
    var supportsUMSettings: Bool {
Bogdan Timofte authored 2 months ago
213
        capabilities.supportsScreenSettings
Bogdan Timofte authored 2 months ago
214
    }
215

            
216
    var supportsRecordingThreshold: Bool {
Bogdan Timofte authored 2 months ago
217
        capabilities.supportsRecordingThreshold
Bogdan Timofte authored 2 months ago
218
    }
219

            
Bogdan Timofte authored 2 months ago
220
    var reportsCurrentScreenIndex: Bool {
221
        capabilities.reportsCurrentScreenIndex
222
    }
223

            
224
    var showsDataGroupEnergy: Bool {
225
        capabilities.showsDataGroupEnergy
226
    }
227

            
228
    var highlightsActiveDataGroup: Bool {
229
        if model == .TC66C {
230
            return hasObservedActiveDataGroup
231
        }
232
        return capabilities.highlightsActiveDataGroup
233
    }
234

            
Bogdan Timofte authored 2 months ago
235
    var supportsFahrenheit: Bool {
Bogdan Timofte authored 2 months ago
236
        capabilities.supportsFahrenheit
Bogdan Timofte authored 2 months ago
237
    }
238

            
Bogdan Timofte authored 2 months ago
239
    var supportsManualTemperatureUnitSelection: Bool {
240
        model == .TC66C
241
    }
242

            
Bogdan Timofte authored 2 months ago
243
    var supportsChargerDetection: Bool {
Bogdan Timofte authored 2 months ago
244
        capabilities.supportsChargerDetection
245
    }
246

            
Bogdan Timofte authored 2 months ago
247
    var dataGroupsTitle: String {
248
        capabilities.dataGroupsTitle
249
    }
250

            
Bogdan Timofte authored 2 months ago
251
    var documentedWorkingVoltage: String {
252
        capabilities.documentedWorkingVoltage
253
    }
254

            
Bogdan Timofte authored 2 months ago
255
    var chargerTypeDescription: String {
256
        capabilities.chargerTypeDescription(for: chargerTypeIndex)
Bogdan Timofte authored 2 months ago
257
    }
258

            
Bogdan Timofte authored 2 months ago
259
    var temperatureUnitDescription: String {
260
        if supportsManualTemperatureUnitSelection {
Bogdan Timofte authored 2 months ago
261
            return "Device-defined"
Bogdan Timofte authored 2 months ago
262
        }
Bogdan Timofte authored 2 months ago
263
        return systemTemperatureUnitPreference.localeTitle
Bogdan Timofte authored 2 months ago
264
    }
265

            
266
    var primaryTemperatureDescription: String {
Bogdan Timofte authored 2 months ago
267
        let value = displayedTemperatureValue.format(decimalDigits: 0)
Bogdan Timofte authored 2 months ago
268
        if supportsManualTemperatureUnitSelection {
Bogdan Timofte authored 2 months ago
269
            return "\(value)°"
Bogdan Timofte authored 2 months ago
270
        }
Bogdan Timofte authored 2 months ago
271
        return "\(value)\(systemTemperatureUnitPreference.symbol)"
Bogdan Timofte authored 2 months ago
272
    }
273

            
274
    var secondaryTemperatureDescription: String? {
Bogdan Timofte authored 2 months ago
275
        nil
276
    }
277

            
278
    var displayedTemperatureValue: Double {
279
        if supportsManualTemperatureUnitSelection {
280
            return temperatureCelsius
281
        }
282
        switch systemTemperatureUnitPreference {
283
        case .celsius:
284
            return displayedTemperatureCelsius
285
        case .fahrenheit:
286
            return displayedTemperatureFahrenheit
287
        }
288
    }
289

            
290
    private var displayedTemperatureCelsius: Double {
291
        if supportsManualTemperatureUnitSelection {
292
            switch tc66TemperatureUnitPreference {
293
            case .celsius:
294
                return temperatureCelsius
295
            case .fahrenheit:
296
                return (temperatureCelsius - 32) * 5 / 9
297
            }
298
        }
299
        return temperatureCelsius
300
    }
301

            
302
    private var displayedTemperatureFahrenheit: Double {
303
        if supportsManualTemperatureUnitSelection {
304
            switch tc66TemperatureUnitPreference {
305
            case .celsius:
306
                return (temperatureCelsius * 9 / 5) + 32
307
            case .fahrenheit:
308
                return temperatureCelsius
309
            }
310
        }
311
        if supportsFahrenheit, temperatureFahrenheit.isFinite {
312
            return temperatureFahrenheit
313
        }
314
        return (temperatureCelsius * 9 / 5) + 32
315
    }
316

            
317
    private var systemTemperatureUnitPreference: TemperatureUnitPreference {
318
        let locale = Locale.autoupdatingCurrent
319
        if #available(iOS 16.0, *) {
320
            switch locale.measurementSystem {
321
            case .us:
322
                return .fahrenheit
323
            default:
324
                return .celsius
325
            }
326
        }
327

            
328
        let regionCode = locale.regionCode ?? ""
329
        let fahrenheitRegions: Set<String> = ["US", "BS", "BZ", "KY", "PW", "LR", "FM", "MH"]
330
        return fahrenheitRegions.contains(regionCode) ? .fahrenheit : .celsius
Bogdan Timofte authored 2 months ago
331
    }
332

            
Bogdan Timofte authored 2 months ago
333
    var currentScreenDescription: String {
Bogdan Timofte authored 2 months ago
334
        guard reportsCurrentScreenIndex else {
335
            return "Page Controls"
336
        }
Bogdan Timofte authored 2 months ago
337
        if let label = capabilities.screenDescription(for: currentScreen) {
338
            return "Screen \(currentScreen): \(label)"
339
        }
340
        return "Screen \(currentScreen)"
341
    }
342

            
Bogdan Timofte authored 2 months ago
343
    var deviceModelName: String {
Bogdan Timofte authored 2 months ago
344
        if !reportedModelName.isEmpty {
345
            return reportedModelName
346
        }
347
        return model.canonicalName
Bogdan Timofte authored 2 months ago
348
    }
349

            
Bogdan Timofte authored 2 months ago
350
    var deviceModelSummary: String {
Bogdan Timofte authored 2 months ago
351
        let baseName = deviceModelName
Bogdan Timofte authored 2 months ago
352
        if modelNumber != 0 {
353
            return "\(baseName) (\(modelNumber))"
354
        }
355
        return baseName
356
    }
357

            
Bogdan Timofte authored 2 months ago
358
    var recordingDurationDescription: String {
359
        let totalSeconds = Int(recordingDuration)
360
        let hours = totalSeconds / 3600
361
        let minutes = (totalSeconds % 3600) / 60
362
        let seconds = totalSeconds % 60
363

            
364
        if hours > 0 {
365
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
366
        }
367
        return String(format: "%02d:%02d", minutes, seconds)
368
    }
369

            
Bogdan Timofte authored 2 months ago
370
    var chargeRecordDurationDescription: String {
371
        let totalSeconds = Int(chargeRecordDuration)
372
        let hours = totalSeconds / 3600
373
        let minutes = (totalSeconds % 3600) / 60
374
        let seconds = totalSeconds % 60
375

            
376
        if hours > 0 {
377
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
378
        }
379
        return String(format: "%02d:%02d", minutes, seconds)
380
    }
381

            
382
    var chargeRecordTimeRange: ClosedRange<Date>? {
383
        guard let start = chargeRecordStartTimestamp else { return nil }
384
        let end = chargeRecordEndTimestamp ?? chargeRecordLastTimestamp
385
        guard let end else { return nil }
386
        return start...end
387
    }
388

            
389
    var chargeRecordStatusText: String {
390
        switch chargeRecordState {
391
        case .waitingForStart:
392
            return "Waiting"
393
        case .active:
394
            return "Active"
395
        case .completed:
396
            return "Completed"
397
        }
398
    }
399

            
400
    var chargeRecordStatusColor: Color {
401
        switch chargeRecordState {
402
        case .waitingForStart:
403
            return .secondary
404
        case .active:
405
            return .red
406
        case .completed:
407
            return .green
408
        }
409
    }
410

            
Bogdan Timofte authored 2 months ago
411
    var dataGroupsHint: String? {
Bogdan Timofte authored 2 months ago
412
        if model == .TC66C {
413
            if hasObservedActiveDataGroup {
414
                return "The active memory is inferred from the totals that are currently increasing."
415
            }
416
            return "The device exposes two read-only memories. The active memory is inferred once one total starts increasing."
417
        }
418
        return capabilities.dataGroupsHint
419
    }
420

            
421
    func dataGroupLabel(for id: UInt8) -> String {
422
        supportsDataGroupCommands ? "\(id)" : "M\(Int(id) + 1)"
Bogdan Timofte authored 2 months ago
423
    }
424

            
425
    var recordingThresholdHint: String? {
426
        capabilities.recordingThresholdHint
427
    }
428

            
Bogdan Timofte authored 2 months ago
429
    var btSerial: BluetoothSerial
Bogdan Timofte authored 2 months ago
430

            
Bogdan Timofte authored 2 months ago
431
    var measurements = Measurements()
Bogdan Timofte authored 2 months ago
432

            
433
    private var commandQueue: [Data] = []
434
    private var dataDumpRequestTimestamp = Date()
Bogdan Timofte authored 2 months ago
435
    private var pendingDataDumpWorkItem: DispatchWorkItem?
Bogdan Timofte authored 2 months ago
436

            
437
    class DataGroupRecord {
Bogdan Timofte authored 2 months ago
438
        var ah: Double
439
        var wh: Double
Bogdan Timofte authored 2 months ago
440
        init(ah: Double, wh: Double) {
441
            self.ah = ah
442
            self.wh = wh
443
        }
444
    }
Bogdan Timofte authored 2 months ago
445
    private(set) var selectedDataGroup: UInt8 = 0
446
    private(set) var dataGroupRecords: [Int : DataGroupRecord] = [:]
447
    private(set) var chargeRecordAH: Double = 0
448
    private(set) var chargeRecordWH: Double = 0
449
    private(set) var chargeRecordDuration: TimeInterval = 0
Bogdan Timofte authored 2 months ago
450
    @Published var chargeRecordStopThreshold: Double = 0.05
451
    @Published var tc66TemperatureUnitPreference: TemperatureUnitPreference = .celsius {
452
        didSet {
453
            guard supportsManualTemperatureUnitSelection else { return }
454
            guard oldValue != tc66TemperatureUnitPreference else { return }
Bogdan Timofte authored 2 months ago
455
            appData.setTemperatureUnitPreference(tc66TemperatureUnitPreference, for: btSerial.macAddress.description)
Bogdan Timofte authored 2 months ago
456
        }
457
    }
Bogdan Timofte authored 2 months ago
458

            
459
    @Published var screenBrightness: Int = -1 {
460
        didSet {
461
            if oldValue != screenBrightness {
462
                screenBrightnessTimestamp = Date()
463
                if oldValue != -1 {
464
                    setSceeenBrightness(to: UInt8(screenBrightness))
465
                }
466
            }
467
        }
468
    }
469
    private var screenBrightnessTimestamp = Date()
470

            
471
    @Published var screenTimeout: Int = -1 {
472
        didSet {
473
            if oldValue != screenTimeout {
474
                screenTimeoutTimestamp = Date()
475
                if oldValue != -1 {
476
                    setScreenSaverTimeout(to: UInt8(screenTimeout))
477
                }
478
            }
479
        }
480
    }
481
    private var screenTimeoutTimestamp = Date()
482

            
Bogdan Timofte authored 2 months ago
483
    private(set) var voltage: Double = 0
484
    private(set) var current: Double = 0
485
    private(set) var power: Double = 0
486
    private(set) var temperatureCelsius: Double = 0
487
    private(set) var temperatureFahrenheit: Double = 0
488
    private(set) var usbPlusVoltage: Double = 0
489
    private(set) var usbMinusVoltage: Double = 0
490
    private(set) var recordedAH: Double = 0
491
    private(set) var recordedWH: Double = 0
492
    private(set) var recording: Bool = false
Bogdan Timofte authored 2 months ago
493
    @Published var recordingTreshold: Double = 0 {
Bogdan Timofte authored 2 months ago
494
        didSet {
Bogdan Timofte authored 2 months ago
495
            guard recordingTreshold != oldValue else { return }
496
            if isApplyingRecordingThresholdFromDevice {
497
                return
Bogdan Timofte authored 2 months ago
498
            }
Bogdan Timofte authored 2 months ago
499
            recordingThresholdTimestamp = Date()
500
            guard recordingThresholdLoadedFromDevice else { return }
501
            setrecordingTreshold(to: (recordingTreshold * 100).uInt8Value)
Bogdan Timofte authored 2 months ago
502
        }
Bogdan Timofte authored 2 months ago
503
    }
Bogdan Timofte authored 2 months ago
504
    private(set) var currentScreen: UInt16 = 0
505
    private(set) var recordingDuration: UInt32 = 0
506
    private(set) var loadResistance: Double = 0
507
    private(set) var modelNumber: UInt16 = 0
508
    private(set) var chargerTypeIndex: UInt16 = 0
509
    private(set) var reportedModelName: String = ""
510
    private(set) var firmwareVersion: String = ""
511
    private(set) var serialNumber: UInt32 = 0
512
    private(set) var bootCount: UInt32 = 0
Bogdan Timofte authored 2 months ago
513
    private var enableAutoConnect: Bool = false
Bogdan Timofte authored 2 months ago
514
    private var recordingThresholdTimestamp = Date()
515
    private var recordingThresholdLoadedFromDevice = false
516
    private var isApplyingRecordingThresholdFromDevice = false
Bogdan Timofte authored 2 months ago
517
    private(set) var chargeRecordState = ChargeRecordState.waitingForStart
Bogdan Timofte authored 2 months ago
518
    private var chargeRecordStartTimestamp: Date?
519
    private var chargeRecordEndTimestamp: Date?
520
    private var chargeRecordLastTimestamp: Date?
521
    private var chargeRecordLastCurrent: Double = 0
522
    private var chargeRecordLastPower: Double = 0
Bogdan Timofte authored 2 months ago
523
    private let volatileMemoryDecreaseEpsilon = 0.0005
524
    private let initiatedVolatileMemoryResetGraceWindow: TimeInterval = 12
525
    private var hasSeenUMSnapshot = false
Bogdan Timofte authored 2 months ago
526
    private var hasObservedActiveDataGroup = false
527
    private var hasSeenTC66Snapshot = false
Bogdan Timofte authored 2 months ago
528
    private var pendingVolatileMemoryResetIgnoreCount = 0
529
    private var pendingVolatileMemoryResetDeadline: Date?
Bogdan Timofte authored 2 months ago
530
    private var liveDataChanged = false
Bogdan Timofte authored a month ago
531
    private var restoredChargeSessionID: UUID?
Bogdan Timofte authored a month ago
532
    private var lastRecorderObservationAt: Date?
Bogdan Timofte authored 2 months ago
533

            
Bogdan Timofte authored 2 months ago
534
    @discardableResult
535
    private func setIfChanged<T: Equatable>(_ keyPath: ReferenceWritableKeyPath<Meter, T>, to value: T) -> Bool {
536
        guard self[keyPath: keyPath] != value else { return false }
537
        self[keyPath: keyPath] = value
538
        liveDataChanged = true
539
        return true
540
    }
541

            
542
    private func updateDataGroupRecord(index: Int, ah: Double, wh: Double) {
543
        if let existing = dataGroupRecords[index] {
544
            if existing.ah != ah { existing.ah = ah; liveDataChanged = true }
545
            if existing.wh != wh { existing.wh = wh; liveDataChanged = true }
546
        } else {
547
            dataGroupRecords[index] = DataGroupRecord(ah: ah, wh: wh)
548
            liveDataChanged = true
549
        }
550
    }
551

            
Bogdan Timofte authored 2 months ago
552
    init ( model: Model, with serialPort: BluetoothSerial ) {
553
        uuid = serialPort.peripheral.identifier
554
        //dataStore.meterUUIDS.append(serialPort.peripheral.identifier)
555
        modelString = serialPort.peripheral.name!
556
        self.model = model
557
        btSerial = serialPort
Bogdan Timofte authored 2 months ago
558
        name = appData.meterName(for: serialPort.macAddress.description) ?? serialPort.macAddress.description
Bogdan Timofte authored 2 months ago
559
        lastSeen = appData.lastSeen(for: serialPort.macAddress.description)
560
        lastConnectedAt = appData.lastConnected(for: serialPort.macAddress.description)
Bogdan Timofte authored 2 months ago
561
        super.init()
562
        btSerial.delegate = self
Bogdan Timofte authored 2 months ago
563
        reloadTemperatureUnitPreference()
Bogdan Timofte authored 2 months ago
564
        //name = dataStore.meterNames[macAddress.description] ?? peripheral.name!
565
        for index in stride(from: 0, through: 9, by: 1) {
566
            dataGroupRecords[index] = DataGroupRecord(ah:0, wh: 0)
567
        }
568
    }
Bogdan Timofte authored 2 months ago
569

            
570
    func reloadTemperatureUnitPreference() {
571
        guard supportsManualTemperatureUnitSelection else { return }
Bogdan Timofte authored 2 months ago
572
        let persistedPreference = appData.temperatureUnitPreference(for: btSerial.macAddress.description)
Bogdan Timofte authored 2 months ago
573
        if tc66TemperatureUnitPreference != persistedPreference {
574
            tc66TemperatureUnitPreference = persistedPreference
575
        }
576
    }
Bogdan Timofte authored 2 months ago
577

            
Bogdan Timofte authored 2 months ago
578
    func updateNameFromStore(_ newName: String) {
579
        guard newName != name else { return }
580
        isSyncingNameFromStore = true
581
        name = newName
582
        isSyncingNameFromStore = false
583
    }
584

            
Bogdan Timofte authored 2 months ago
585
    private func noteConnectionEstablished(at date: Date) {
586
        lastConnectedAt = date
587
        appData.noteMeterConnected(at: date, macAddress: btSerial.macAddress.description)
588
    }
589

            
Bogdan Timofte authored 2 months ago
590
    private func handleMeasurementDiscontinuity(at timestamp: Date) {
591
        measurements.markDiscontinuity(at: timestamp)
592

            
593
        guard chargeRecordState == .active else { return }
594
        chargeRecordLastTimestamp = nil
595
        chargeRecordLastCurrent = 0
596
        chargeRecordLastPower = 0
597
    }
598

            
Bogdan Timofte authored 2 months ago
599
    private func currentEnergySample() -> (groupID: UInt8, value: Double)? {
600
        guard showsDataGroupEnergy else { return nil }
601

            
602
        if model == .TC66C && !hasObservedActiveDataGroup {
603
            return nil
604
        }
605

            
606
        let groupID = selectedDataGroup
607
        guard let record = dataGroupRecords[Int(groupID)] else { return nil }
608
        return (groupID, record.wh)
609
    }
610

            
Bogdan Timofte authored a month ago
611
    private func currentChargeSample() -> (groupID: UInt8, value: Double)? {
612
        guard showsDataGroupEnergy else { return nil }
613

            
614
        if model == .TC66C && !hasObservedActiveDataGroup {
615
            return nil
616
        }
617

            
618
        let groupID = selectedDataGroup
619
        guard let record = dataGroupRecords[Int(groupID)] else { return nil }
620
        return (groupID, record.ah)
621
    }
622

            
623
    func chargingMonitorSnapshot(at observedAt: Date) -> ChargingMonitorSnapshot? {
Bogdan Timofte authored a month ago
624
        let usesNativeRecordingCounters = supportsRecordingView
625
        let nativeChargeCounter = usesNativeRecordingCounters ? recordedAH : nil
626
        let nativeEnergyCounter = usesNativeRecordingCounters ? recordedWH : nil
627

            
628
        return ChargingMonitorSnapshot(
Bogdan Timofte authored a month ago
629
            meterMACAddress: btSerial.macAddress.description,
630
            meterName: name,
631
            meterModel: deviceModelSummary,
632
            observedAt: observedAt,
633
            voltageVolts: voltage,
634
            currentAmps: current,
635
            powerWatts: power,
Bogdan Timofte authored a month ago
636
            selectedDataGroup: usesNativeRecordingCounters ? nil : (currentEnergySample()?.groupID ?? currentChargeSample()?.groupID),
637
            meterChargeCounterAh: nativeChargeCounter ?? currentChargeSample()?.value,
638
            meterEnergyCounterWh: nativeEnergyCounter ?? currentEnergySample()?.value,
Bogdan Timofte authored a month ago
639
            meterRecordingDurationSeconds: usesNativeRecordingCounters ? TimeInterval(recordingDuration) : nil,
Bogdan Timofte authored a month ago
640
            fallbackStopThresholdAmps: supportsRecordingThreshold ? recordingTreshold : chargeRecordStopThreshold
Bogdan Timofte authored a month ago
641
        )
642
    }
643

            
Bogdan Timofte authored a month ago
644
    var recordingBootedAt: Date? {
645
        guard supportsRecordingView else { return nil }
646
        guard let lastRecorderObservationAt else { return nil }
647
        return lastRecorderObservationAt.addingTimeInterval(-TimeInterval(recordingDuration))
648
    }
649

            
Bogdan Timofte authored a month ago
650
    var chargingMonitorSnapshot: ChargingMonitorSnapshot? {
651
        chargingMonitorSnapshot(at: Date())
652
    }
653

            
Bogdan Timofte authored 2 months ago
654
    private func cancelPendingDataDumpRequest(reason: String) {
655
        guard let pendingDataDumpWorkItem else { return }
656
        track("\(name) - Cancel scheduled data request (\(reason))")
657
        pendingDataDumpWorkItem.cancel()
658
        self.pendingDataDumpWorkItem = nil
659
    }
660

            
661
    private func scheduleDataDumpRequest(after delay: TimeInterval, reason: String) {
662
        cancelPendingDataDumpRequest(reason: "reschedule")
663

            
664
        let workItem = DispatchWorkItem { [weak self] in
665
            guard let self else { return }
666
            self.pendingDataDumpWorkItem = nil
667
            self.dataDumpRequest()
668
        }
669
        pendingDataDumpWorkItem = workItem
670
        track("\(name) - Schedule data request in \(String(format: "%.2f", delay))s (\(reason))")
671
        DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
672
    }
Bogdan Timofte authored 2 months ago
673

            
674
    private func noteInitiatedVolatileMemoryResetIfNeeded(for groupID: UInt8) {
675
        guard groupID == 0 else { return }
676
        pendingVolatileMemoryResetIgnoreCount += 1
677
        pendingVolatileMemoryResetDeadline = Date().addingTimeInterval(initiatedVolatileMemoryResetGraceWindow)
678
        track("\(name) - Will ignore the next volatile memory drop caused by an app reset request.")
679
    }
680

            
681
    private func pendingInitiatedVolatileMemoryResetIsActive(at timestamp: Date) -> Bool {
682
        guard let pendingVolatileMemoryResetDeadline else { return false }
683
        guard pendingVolatileMemoryResetIgnoreCount > 0 else {
684
            self.pendingVolatileMemoryResetDeadline = nil
685
            return false
686
        }
687
        guard timestamp <= pendingVolatileMemoryResetDeadline else {
688
            track("\(name) - Expiring stale volatile memory reset ignore state.")
689
            pendingVolatileMemoryResetIgnoreCount = 0
690
            self.pendingVolatileMemoryResetDeadline = nil
691
            return false
692
        }
693
        return true
694
    }
695

            
696
    private func shouldIgnoreVolatileMemoryDrop(at timestamp: Date) -> Bool {
697
        guard pendingInitiatedVolatileMemoryResetIsActive(at: timestamp) else { return false }
698
        pendingVolatileMemoryResetIgnoreCount -= 1
699
        if pendingVolatileMemoryResetIgnoreCount == 0 {
700
            pendingVolatileMemoryResetDeadline = nil
701
        }
702
        track("\(name) - Ignoring volatile memory drop after an app-initiated reset.")
703
        return true
704
    }
705

            
706
    private func didUMVolatileMemoryDecrease(in snapshot: UMSnapshot) -> Bool {
707
        guard hasSeenUMSnapshot else { return false }
708
        guard let previousRecord = dataGroupRecords[0], let nextRecord = snapshot.dataGroupRecords[0] else {
709
            return false
710
        }
711

            
712
        return nextRecord.ah < (previousRecord.ah - volatileMemoryDecreaseEpsilon)
713
            || nextRecord.wh < (previousRecord.wh - volatileMemoryDecreaseEpsilon)
714
    }
715

            
716
    private func didUMDeviceReboot(with snapshot: UMSnapshot, at timestamp: Date) -> Bool {
717
        defer { hasSeenUMSnapshot = true }
718

            
719
        guard didUMVolatileMemoryDecrease(in: snapshot) else { return false }
720
        guard !shouldIgnoreVolatileMemoryDrop(at: timestamp) else { return false }
721

            
722
        track("\(name) - Inferred UM reboot because volatile memory dropped.")
723
        return true
724
    }
725

            
726
    private func didTC66DeviceReboot(with snapshot: TC66Snapshot) -> Bool {
727
        guard hasSeenTC66Snapshot else { return false }
728
        guard snapshot.bootCount != bootCount else { return false }
729

            
730
        track("\(name) - Inferred TC66 reboot because bootCount changed from \(bootCount) to \(snapshot.bootCount).")
731
        return true
732
    }
733

            
734
    private func updateChargerTypeLatch(observedIndex: UInt16, didDetectDeviceReset: Bool) {
735
        if didDetectDeviceReset, chargerTypeIndex != 0 {
Bogdan Timofte authored 2 months ago
736
            setIfChanged(\.chargerTypeIndex, to: 0)
Bogdan Timofte authored 2 months ago
737
        }
738

            
739
        guard supportsChargerDetection else { return }
740

            
741
        if chargerTypeIndex == 0 {
Bogdan Timofte authored 2 months ago
742
            setIfChanged(\.chargerTypeIndex, to: observedIndex)
Bogdan Timofte authored 2 months ago
743
            return
744
        }
745

            
746
        guard observedIndex != 0, observedIndex != chargerTypeIndex else { return }
747
        track("\(name) - Ignoring charger type change from \(chargerTypeIndex) to \(observedIndex) until the device reboots.")
748
    }
Bogdan Timofte authored 2 months ago
749

            
750
    func dataDumpRequest() {
Bogdan Timofte authored 2 months ago
751
        guard operationalState >= .peripheralReady else {
752
            track("\(name) - Skip data request while state is \(operationalState)")
753
            return
754
        }
Bogdan Timofte authored 2 months ago
755
        if commandQueue.isEmpty {
756
            switch model {
757
            case .UM25C:
Bogdan Timofte authored 2 months ago
758
                btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
Bogdan Timofte authored 2 months ago
759
            case .UM34C:
Bogdan Timofte authored 2 months ago
760
                btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
Bogdan Timofte authored 2 months ago
761
            case .TC66C:
Bogdan Timofte authored 2 months ago
762
                btSerial.write(TC66Protocol.snapshotRequest, expectedResponseLength: 192)
Bogdan Timofte authored 2 months ago
763
            }
764
            dataDumpRequestTimestamp = Date()
765
            // track("\(name) - Request sent!")
766
        } else {
767
            track("Request delayed for: \(commandQueue.first!.hexEncodedStringValue)")
768
            btSerial.write( commandQueue.first! )
769
            commandQueue.removeFirst()
Bogdan Timofte authored 2 months ago
770
            scheduleDataDumpRequest(after: 1, reason: "queued command")
Bogdan Timofte authored 2 months ago
771
        }
772
    }
773

            
774
    /**
775
     received data parser
776
     - parameter buffer cotains response for data dump request
777
     - Decription metod for TC66C AES ECB response  found [here](https:github.com/krzyzanowskim/CryptoSwift/issues/693)
778
     */
779
    func parseData ( from buffer: Data) {
780
        //track("\(name)")
Bogdan Timofte authored 2 months ago
781
        liveDataChanged = false
Bogdan Timofte authored 2 months ago
782
        switch model {
783
        case .UM25C:
Bogdan Timofte authored 2 months ago
784
            do {
785
                apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
786
            } catch {
787
                track("\(name) - Error: \(error)")
788
            }
Bogdan Timofte authored 2 months ago
789
        case .UM34C:
Bogdan Timofte authored 2 months ago
790
            do {
791
                apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
792
            } catch {
793
                track("\(name) - Error: \(error)")
794
            }
Bogdan Timofte authored 2 months ago
795
        case .TC66C:
Bogdan Timofte authored 2 months ago
796
            do {
797
                apply(tc66Snapshot: try TC66Protocol.parseSnapshot(from: buffer))
798
            } catch {
799
                track("\(name) - Error: \(error)")
800
            }
Bogdan Timofte authored 2 months ago
801
        }
Bogdan Timofte authored 2 months ago
802
        updateChargeRecord(at: dataDumpRequestTimestamp)
Bogdan Timofte authored a month ago
803
        if supportsRecordingView {
804
            measurements.captureEnergyValue(
805
                timestamp: dataDumpRequestTimestamp,
806
                value: recordedWH,
807
                groupID: .max
808
            )
809
        } else if let energySample = currentEnergySample() {
Bogdan Timofte authored 2 months ago
810
            measurements.captureEnergyValue(
811
                timestamp: dataDumpRequestTimestamp,
812
                value: energySample.value,
813
                groupID: energySample.groupID
814
            )
815
        }
Bogdan Timofte authored 2 months ago
816
        measurements.addValues(
817
            timestamp: dataDumpRequestTimestamp,
818
            power: power,
819
            voltage: voltage,
820
            current: current,
821
            temperature: displayedTemperatureValue,
822
            rssi: Double(btSerial.averageRSSI)
823
        )
Bogdan Timofte authored a month ago
824
        appData.observeChargeSnapshot(from: self, observedAt: dataDumpRequestTimestamp)
Bogdan Timofte authored 2 months ago
825
//        DispatchQueue.global(qos: .userInitiated).asyncAfter( deadline: .now() + 0.33 ) {
826
//            //track("\(name) - Scheduled new request.")
827
//        }
Bogdan Timofte authored 2 months ago
828
        if operationalState != .dataIsAvailable {
829
            operationalState = .dataIsAvailable
830
        } else if liveDataChanged {
831
            objectWillChange.send()
832
        }
Bogdan Timofte authored 2 months ago
833
        dataDumpRequest()
834
    }
835

            
Bogdan Timofte authored 2 months ago
836
    private func apply(umSnapshot snapshot: UMSnapshot) {
Bogdan Timofte authored 2 months ago
837
        let didDetectDeviceReset = didUMDeviceReboot(with: snapshot, at: dataDumpRequestTimestamp)
Bogdan Timofte authored a month ago
838
        lastRecorderObservationAt = dataDumpRequestTimestamp
Bogdan Timofte authored 2 months ago
839
        setIfChanged(\.modelNumber, to: snapshot.modelNumber)
840
        setIfChanged(\.voltage, to: snapshot.voltage)
841
        setIfChanged(\.current, to: snapshot.current)
842
        setIfChanged(\.power, to: snapshot.power)
843
        setIfChanged(\.temperatureCelsius, to: snapshot.temperatureCelsius)
844
        setIfChanged(\.temperatureFahrenheit, to: snapshot.temperatureFahrenheit)
845
        setIfChanged(\.selectedDataGroup, to: snapshot.selectedDataGroup)
Bogdan Timofte authored 2 months ago
846
        for (index, record) in snapshot.dataGroupRecords {
Bogdan Timofte authored 2 months ago
847
            updateDataGroupRecord(index: index, ah: record.ah, wh: record.wh)
Bogdan Timofte authored 2 months ago
848
        }
Bogdan Timofte authored 2 months ago
849
        setIfChanged(\.usbPlusVoltage, to: snapshot.usbPlusVoltage)
850
        setIfChanged(\.usbMinusVoltage, to: snapshot.usbMinusVoltage)
Bogdan Timofte authored 2 months ago
851
        updateChargerTypeLatch(observedIndex: snapshot.chargerTypeIndex, didDetectDeviceReset: didDetectDeviceReset)
Bogdan Timofte authored 2 months ago
852
        setIfChanged(\.recordedAH, to: snapshot.recordedAH)
853
        setIfChanged(\.recordedWH, to: snapshot.recordedWH)
Bogdan Timofte authored 2 months ago
854

            
855
        if recordingThresholdTimestamp < dataDumpRequestTimestamp || !recordingThresholdLoadedFromDevice {
856
            recordingThresholdLoadedFromDevice = true
857
            if recordingTreshold != snapshot.recordingThreshold {
858
                isApplyingRecordingThresholdFromDevice = true
859
                recordingTreshold = snapshot.recordingThreshold
860
                isApplyingRecordingThresholdFromDevice = false
861
            }
862
        } else {
863
            track("\(name) - Skip updating recordingThreshold (changed after request).")
864
        }
Bogdan Timofte authored 2 months ago
865
        setIfChanged(\.recordingDuration, to: snapshot.recordingDuration)
866
        setIfChanged(\.recording, to: snapshot.recording)
Bogdan Timofte authored 2 months ago
867

            
Bogdan Timofte authored 2 months ago
868
        if screenTimeoutTimestamp < dataDumpRequestTimestamp {
Bogdan Timofte authored 2 months ago
869
            if screenTimeout != snapshot.screenTimeout {
870
                screenTimeout = snapshot.screenTimeout
Bogdan Timofte authored 2 months ago
871
            }
872
        } else {
873
            track("\(name) - Skip updating screenTimeout (changed after request).")
874
        }
875

            
876
        if screenBrightnessTimestamp < dataDumpRequestTimestamp {
Bogdan Timofte authored 2 months ago
877
            if screenBrightness != snapshot.screenBrightness {
878
                screenBrightness = snapshot.screenBrightness
Bogdan Timofte authored 2 months ago
879
            }
880
        } else {
881
            track("\(name) - Skip updating screenBrightness (changed after request).")
882
        }
883

            
Bogdan Timofte authored 2 months ago
884
        setIfChanged(\.currentScreen, to: snapshot.currentScreen)
885
        setIfChanged(\.loadResistance, to: snapshot.loadResistance)
Bogdan Timofte authored 2 months ago
886
    }
887

            
Bogdan Timofte authored 2 months ago
888
    private func apply(tc66Snapshot snapshot: TC66Snapshot) {
Bogdan Timofte authored 2 months ago
889
        let didDetectDeviceReset = didTC66DeviceReboot(with: snapshot)
Bogdan Timofte authored 2 months ago
890
        if hasSeenTC66Snapshot {
891
            inferTC66ActiveDataGroup(from: snapshot)
892
        } else {
893
            hasSeenTC66Snapshot = true
894
        }
Bogdan Timofte authored 2 months ago
895
        setIfChanged(\.reportedModelName, to: snapshot.modelName)
896
        setIfChanged(\.firmwareVersion, to: snapshot.firmwareVersion)
897
        setIfChanged(\.serialNumber, to: snapshot.serialNumber)
898
        setIfChanged(\.bootCount, to: snapshot.bootCount)
Bogdan Timofte authored 2 months ago
899
        updateChargerTypeLatch(observedIndex: 0, didDetectDeviceReset: didDetectDeviceReset)
Bogdan Timofte authored 2 months ago
900
        setIfChanged(\.voltage, to: snapshot.voltage)
901
        setIfChanged(\.current, to: snapshot.current)
902
        setIfChanged(\.power, to: snapshot.power)
903
        setIfChanged(\.loadResistance, to: snapshot.loadResistance)
Bogdan Timofte authored 2 months ago
904
        for (index, record) in snapshot.dataGroupRecords {
Bogdan Timofte authored 2 months ago
905
            updateDataGroupRecord(index: index, ah: record.ah, wh: record.wh)
Bogdan Timofte authored 2 months ago
906
        }
Bogdan Timofte authored 2 months ago
907
        setIfChanged(\.temperatureCelsius, to: snapshot.temperatureCelsius)
908
        setIfChanged(\.usbPlusVoltage, to: snapshot.usbPlusVoltage)
909
        setIfChanged(\.usbMinusVoltage, to: snapshot.usbMinusVoltage)
Bogdan Timofte authored 2 months ago
910
    }
Bogdan Timofte authored 2 months ago
911

            
912
    private func inferTC66ActiveDataGroup(from snapshot: TC66Snapshot) {
913
        let candidate = snapshot.dataGroupRecords.compactMap { entry -> (UInt8, Double)? in
914
            let index = entry.key
915
            let record = entry.value
916
            guard let previous = dataGroupRecords[index] else { return nil }
917
            let deltaAH = max(record.ah - previous.ah, 0)
918
            let deltaWH = max(record.wh - previous.wh, 0)
919
            let score = deltaAH + deltaWH
920
            guard score > 0 else { return nil }
921
            return (UInt8(index), score)
922
        }
923
        .max { lhs, rhs in lhs.1 < rhs.1 }
924

            
925
        if let candidate {
926
            selectedDataGroup = candidate.0
927
            hasObservedActiveDataGroup = true
928
        }
929
    }
930

            
931
    private func updateChargeRecord(at timestamp: Date) {
932
        switch chargeRecordState {
933
        case .waitingForStart:
934
            guard current > chargeRecordStopThreshold else { return }
935
            chargeRecordState = .active
936
            chargeRecordStartTimestamp = timestamp
937
            chargeRecordEndTimestamp = timestamp
938
            chargeRecordLastTimestamp = timestamp
939
            chargeRecordLastCurrent = current
940
            chargeRecordLastPower = power
941
        case .active:
942
            if let lastTimestamp = chargeRecordLastTimestamp {
943
                let deltaSeconds = max(timestamp.timeIntervalSince(lastTimestamp), 0)
944
                chargeRecordAH += chargeRecordLastCurrent * deltaSeconds / 3600
945
                chargeRecordWH += chargeRecordLastPower * deltaSeconds / 3600
946
                chargeRecordDuration += deltaSeconds
947
            }
948
            chargeRecordEndTimestamp = timestamp
949
            chargeRecordLastTimestamp = timestamp
950
            chargeRecordLastCurrent = current
951
            chargeRecordLastPower = power
952
            if current <= chargeRecordStopThreshold {
953
                chargeRecordState = .completed
954
            }
955
        case .completed:
956
            break
957
        }
958
    }
959

            
960
    func resetChargeRecord() {
961
        chargeRecordAH = 0
962
        chargeRecordWH = 0
963
        chargeRecordDuration = 0
964
        chargeRecordState = .waitingForStart
965
        chargeRecordStartTimestamp = nil
966
        chargeRecordEndTimestamp = nil
967
        chargeRecordLastTimestamp = nil
968
        chargeRecordLastCurrent = 0
969
        chargeRecordLastPower = 0
Bogdan Timofte authored 2 months ago
970
        objectWillChange.send()
Bogdan Timofte authored 2 months ago
971
    }
972

            
973
    func resetChargeRecordGraph() {
974
        let cutoff = Date()
975
        resetChargeRecord()
976
        measurements.trim(before: cutoff)
977
    }
Bogdan Timofte authored a month ago
978

            
979
    func restoreChargeRecordIfNeeded(from activeSession: ChargeSessionSummary) {
980
        guard chargeRecordState == .waitingForStart else { return }
981
        guard chargeRecordStartTimestamp == nil else { return }
982
        guard chargeRecordAH == 0, chargeRecordWH == 0, chargeRecordDuration == 0 else { return }
983

            
Bogdan Timofte authored a month ago
984
        measurements.restorePersistedChargeSessionSamplesIfNeeded(from: activeSession)
Bogdan Timofte authored a month ago
985
        chargeRecordState = .active
986
        chargeRecordAH = activeSession.measuredChargeAh
987
        chargeRecordWH = activeSession.measuredEnergyWh
Bogdan Timofte authored a month ago
988
        chargeRecordDuration = max(activeSession.effectiveDuration, 0)
Bogdan Timofte authored a month ago
989
        chargeRecordStopThreshold = activeSession.stopThresholdAmps
990
        chargeRecordStartTimestamp = activeSession.startedAt
991
        chargeRecordEndTimestamp = activeSession.lastObservedAt
992
        chargeRecordLastTimestamp = nil
993
        chargeRecordLastCurrent = 0
994
        chargeRecordLastPower = 0
995
        if let selectedDataGroup = activeSession.selectedDataGroup {
996
            self.selectedDataGroup = selectedDataGroup
997
        }
998
        objectWillChange.send()
999
    }
1000

            
1001
    func restoreChargeMonitoringIfNeeded(from activeSession: ChargeSessionSummary) {
1002
        restoreChargeRecordIfNeeded(from: activeSession)
1003

            
1004
        guard restoredChargeSessionID != activeSession.id else {
1005
            return
1006
        }
1007

            
1008
        restoredChargeSessionID = activeSession.id
1009
        enableAutoConnect = true
1010

            
1011
        guard operationalState < .peripheralConnectionPending else {
1012
            return
1013
        }
1014

            
1015
        track("\(name) - Restoring active charge session and reconnecting to meter")
1016
        btSerial.connect()
1017
    }
Bogdan Timofte authored 2 months ago
1018

            
1019
    func nextScreen() {
1020
        switch model {
1021
        case .UM25C:
Bogdan Timofte authored 2 months ago
1022
            commandQueue.append(UMProtocol.nextScreen)
Bogdan Timofte authored 2 months ago
1023
        case .UM34C:
Bogdan Timofte authored 2 months ago
1024
            commandQueue.append(UMProtocol.nextScreen)
Bogdan Timofte authored 2 months ago
1025
        case .TC66C:
Bogdan Timofte authored 2 months ago
1026
            commandQueue.append(TC66Protocol.nextPage)
Bogdan Timofte authored 2 months ago
1027
        }
1028
    }
1029

            
1030
    func rotateScreen() {
1031
        switch model {
1032
        case .UM25C:
Bogdan Timofte authored 2 months ago
1033
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 months ago
1034
        case .UM34C:
Bogdan Timofte authored 2 months ago
1035
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 months ago
1036
        case .TC66C:
Bogdan Timofte authored 2 months ago
1037
            commandQueue.append(TC66Protocol.rotateScreen)
Bogdan Timofte authored 2 months ago
1038
        }
1039
    }
1040

            
1041
    func previousScreen() {
1042
        switch model {
1043
        case .UM25C:
Bogdan Timofte authored 2 months ago
1044
            commandQueue.append(UMProtocol.previousScreen)
Bogdan Timofte authored 2 months ago
1045
        case .UM34C:
Bogdan Timofte authored 2 months ago
1046
            commandQueue.append(UMProtocol.previousScreen)
Bogdan Timofte authored 2 months ago
1047
        case .TC66C:
Bogdan Timofte authored 2 months ago
1048
            commandQueue.append(TC66Protocol.previousPage)
Bogdan Timofte authored 2 months ago
1049
        }
1050
    }
1051

            
1052
    func clear() {
Bogdan Timofte authored 2 months ago
1053
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 months ago
1054
        noteInitiatedVolatileMemoryResetIfNeeded(for: selectedDataGroup)
Bogdan Timofte authored 2 months ago
1055
        commandQueue.append(UMProtocol.clearCurrentGroup)
Bogdan Timofte authored 2 months ago
1056
    }
Bogdan Timofte authored a month ago
1057

            
1058
    func resetMeterCountersForNewSession() {
1059
        guard supportsDataGroupCommands else { return }
1060

            
1061
        clear()
1062

            
1063
        if let record = dataGroupRecords[Int(selectedDataGroup)] {
1064
            record.ah = 0
1065
            record.wh = 0
1066
        }
1067
        recordedAH = 0
1068
        recordedWH = 0
1069
        recording = false
1070
        objectWillChange.send()
1071
    }
Bogdan Timofte authored 2 months ago
1072

            
1073
    func clear(group id: UInt8) {
Bogdan Timofte authored 2 months ago
1074
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 months ago
1075
        commandQueue.append(UMProtocol.selectDataGroup(id))
Bogdan Timofte authored 2 months ago
1076
        noteInitiatedVolatileMemoryResetIfNeeded(for: id)
1077
        commandQueue.append(UMProtocol.clearCurrentGroup)
Bogdan Timofte authored 2 months ago
1078
        commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
Bogdan Timofte authored 2 months ago
1079
    }
1080

            
1081
    func selectDataGroup ( id: UInt8) {
Bogdan Timofte authored 2 months ago
1082
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 months ago
1083
        track("\(name) - \(id)")
1084
        selectedDataGroup = id
Bogdan Timofte authored 2 months ago
1085
        objectWillChange.send()
Bogdan Timofte authored 2 months ago
1086
        commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
Bogdan Timofte authored 2 months ago
1087
    }
1088

            
1089
    private func setSceeenBrightness ( to value: UInt8) {
1090
        track("\(name) - \(value)")
Bogdan Timofte authored 2 months ago
1091
        guard supportsUMSettings else { return }
Bogdan Timofte authored 2 months ago
1092
        commandQueue.append(UMProtocol.setScreenBrightness(value))
Bogdan Timofte authored 2 months ago
1093
    }
1094
    private func setScreenSaverTimeout ( to value: UInt8) {
1095
        track("\(name) - \(value)")
Bogdan Timofte authored 2 months ago
1096
        guard supportsUMSettings else { return }
Bogdan Timofte authored 2 months ago
1097
        commandQueue.append(UMProtocol.setScreenSaverTimeout(value))
Bogdan Timofte authored 2 months ago
1098
    }
1099
    func setrecordingTreshold ( to value: UInt8) {
Bogdan Timofte authored 2 months ago
1100
        guard supportsRecordingThreshold else { return }
Bogdan Timofte authored 2 months ago
1101
        commandQueue.append(UMProtocol.setRecordingThreshold(value))
Bogdan Timofte authored 2 months ago
1102
    }
1103

            
1104
    /**
1105
     Connect to meter.
1106
     1. It calls BluetoothSerial.connect
1107
     */
1108
    func connect() {
1109
        enableAutoConnect = true
1110
        btSerial.connect()
1111
    }
1112

            
1113
    /**
1114
     Disconnect from meter.
1115
        It calls BluetoothSerial.disconnect
1116
     */
1117
    func disconnect() {
1118
        enableAutoConnect = false
1119
        btSerial.disconnect()
1120
    }
1121
}
1122

            
1123
extension Meter : SerialPortDelegate {
1124

            
1125
    func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
Bogdan Timofte authored 2 months ago
1126
        let applyStateChange = {
1127
            self.lastSeen = Date()
1128
            switch serialPortOperationalState {
1129
            case .peripheralNotConnected:
1130
                self.operationalState = .peripheralNotConnected
1131
            case .peripheralConnectionPending:
1132
                self.operationalState = .peripheralConnectionPending
1133
            case .peripheralConnected:
Bogdan Timofte authored 2 months ago
1134
                self.noteConnectionEstablished(at: Date())
Bogdan Timofte authored 2 months ago
1135
                self.operationalState = .peripheralConnected
1136
            case .peripheralReady:
1137
                self.operationalState = .peripheralReady
1138
            }
1139
        }
1140

            
1141
        if Thread.isMainThread {
1142
            applyStateChange()
1143
        } else {
1144
            DispatchQueue.main.async(execute: applyStateChange)
Bogdan Timofte authored 2 months ago
1145
        }
1146
    }
1147

            
1148
    func didReceiveData(_ data: Data) {
Bogdan Timofte authored 2 months ago
1149
        let applyData = {
1150
            self.lastSeen = Date()
Bogdan Timofte authored 2 months ago
1151
            if self.operationalState < .comunicating {
1152
                self.operationalState = .comunicating
1153
            }
Bogdan Timofte authored 2 months ago
1154
            self.parseData(from: data)
1155
        }
1156

            
1157
        if Thread.isMainThread {
1158
            applyData()
1159
        } else {
1160
            DispatchQueue.main.async(execute: applyData)
1161
        }
Bogdan Timofte authored 2 months ago
1162
    }
1163
}