Newer Older
1022 lines | 36.564kb
Bogdan Timofte authored 2 weeks 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 2 weeks ago
25
enum Model: CaseIterable {
Bogdan Timofte authored 2 weeks ago
26
    case UM25C
27
    case UM34C
28
    case TC66C
29
}
30

            
Bogdan Timofte authored 2 weeks 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 weeks 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 weeks ago
67
enum ChargeRecordState {
68
    case waitingForStart
69
    case active
70
    case completed
71
}
72

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

            
Bogdan Timofte authored 2 weeks 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 weeks ago
84
    enum OperationalState: Int, Comparable {
Bogdan Timofte authored a week ago
85
        case offline = 0
86
        case connectedElsewhere = 1
87
        case peripheralNotConnected = 2
88
        case peripheralConnectionPending = 3
89
        case peripheralConnected = 4
90
        case peripheralReady = 5
91
        case comunicating = 6
92
        case dataIsAvailable = 7
Bogdan Timofte authored 2 weeks ago
93

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

            
99
    @Published var operationalState = OperationalState.peripheralNotConnected {
100
        didSet {
Bogdan Timofte authored 2 weeks ago
101
            guard operationalState != oldValue else { return }
Bogdan Timofte authored 2 weeks ago
102
            if Self.shouldLogOperationalStateTransition(from: oldValue, to: operationalState) {
103
                track("\(name) - Operational state changed from \(oldValue) to \(operationalState)")
104
            }
Bogdan Timofte authored 2 weeks ago
105
            switch operationalState {
Bogdan Timofte authored a week ago
106
            case .offline:
107
                cancelPendingDataDumpRequest(reason: "meter offline")
108
                stopConnectionRenewal()
109
                appData.clearMeterConnection(macAddress: btSerial.macAddress.description)
110
                break
111
            case .connectedElsewhere:
112
                cancelPendingDataDumpRequest(reason: "connected elsewhere")
113
                stopConnectionRenewal()
Bogdan Timofte authored 2 weeks ago
114
                break
115
            case .peripheralNotConnected:
Bogdan Timofte authored 2 weeks ago
116
                cancelPendingDataDumpRequest(reason: "peripheral disconnected")
Bogdan Timofte authored a week ago
117
                stopConnectionRenewal()
118
                appData.clearMeterConnection(macAddress: btSerial.macAddress.description)
Bogdan Timofte authored 2 weeks ago
119
                if !commandQueue.isEmpty {
120
                    track("\(name) - Clearing \(commandQueue.count) queued commands after disconnect")
121
                    commandQueue.removeAll()
122
                }
Bogdan Timofte authored 2 weeks ago
123
                if enableAutoConnect {
124
                    track("\(name) - Reconnecting...")
125
                    btSerial.connect()
126
                }
127
            case .peripheralConnectionPending:
Bogdan Timofte authored 2 weeks ago
128
                cancelPendingDataDumpRequest(reason: "connection pending")
Bogdan Timofte authored 2 weeks ago
129
                break
130
            case .peripheralConnected:
Bogdan Timofte authored 2 weeks ago
131
                cancelPendingDataDumpRequest(reason: "services not ready yet")
Bogdan Timofte authored a week ago
132
                appData.publishMeterConnection(macAddress: btSerial.macAddress.description, modelType: modelString)
133
                startConnectionRenewal()
Bogdan Timofte authored 2 weeks ago
134
                break
135
            case .peripheralReady:
Bogdan Timofte authored 2 weeks ago
136
                scheduleDataDumpRequest(after: 0.5, reason: "peripheral ready")
Bogdan Timofte authored 2 weeks ago
137
            case .comunicating:
138
                break
139
            case .dataIsAvailable:
140
                break
141
            }
142
        }
143
    }
144

            
145
    static func operationalColor(for state: OperationalState) -> Color {
146
        switch state {
Bogdan Timofte authored a week ago
147
        case .offline:
148
            return .secondary
149
        case .connectedElsewhere:
150
            return .indigo
Bogdan Timofte authored 2 weeks ago
151
        case .peripheralNotConnected:
152
            return .blue
153
        case .peripheralConnectionPending:
154
            return .yellow
155
        case .peripheralConnected:
156
            return .yellow
157
        case .peripheralReady:
158
            return .orange
159
        case .comunicating:
160
            return .orange
161
        case .dataIsAvailable:
162
            return .green
163
        }
164
    }
165

            
166
    private var wdTimer: Timer?
Bogdan Timofte authored a week ago
167
    private var connectionRenewalTimer: Timer?
168

            
169
    private func startConnectionRenewal() {
170
        connectionRenewalTimer?.invalidate()
171
        connectionRenewalTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
172
            guard let self else { return }
173
            appData.publishMeterConnection(macAddress: self.btSerial.macAddress.description, modelType: self.modelString)
174
        }
175
    }
176

            
177
    private func stopConnectionRenewal() {
178
        connectionRenewalTimer?.invalidate()
179
        connectionRenewalTimer = nil
180
    }
Bogdan Timofte authored 2 weeks ago
181

            
Bogdan Timofte authored 2 weeks ago
182
    var lastSeen = Date() {
Bogdan Timofte authored 2 weeks ago
183
        didSet {
184
            wdTimer?.invalidate()
185
            if operationalState == .peripheralNotConnected {
186
                wdTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false, block: {_ in
Bogdan Timofte authored a week ago
187
                    track("\(self.name) - Lost advertisements...")
188
                    self.operationalState = .offline
Bogdan Timofte authored 2 weeks ago
189
                })
Bogdan Timofte authored a week ago
190
            } else if operationalState == .offline {
Bogdan Timofte authored 2 weeks ago
191
               operationalState = .peripheralNotConnected
192
            }
193
        }
194
    }
195

            
196
    var uuid: UUID
197
    var model: Model
198
    var modelString: String
199

            
200
    var name: String {
201
        didSet {
Bogdan Timofte authored a week ago
202
            appData.persistMeterName(name, for: btSerial.macAddress.description)
Bogdan Timofte authored 2 weeks ago
203
        }
204
    }
205

            
206
    var color : Color {
207
        get {
Bogdan Timofte authored 2 weeks ago
208
            return model.color
Bogdan Timofte authored 2 weeks ago
209
        }
210
    }
211

            
Bogdan Timofte authored 2 weeks ago
212
    var capabilities: MeterCapabilities {
213
        model.capabilities
214
    }
215

            
Bogdan Timofte authored 2 weeks ago
216
    var availableDataGroupIDs: [UInt8] {
Bogdan Timofte authored 2 weeks ago
217
        capabilities.availableDataGroupIDs
Bogdan Timofte authored 2 weeks ago
218
    }
219

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

            
Bogdan Timofte authored 2 weeks ago
224
    var supportsRecordingView: Bool {
225
        capabilities.supportsRecordingView
226
    }
227

            
Bogdan Timofte authored 2 weeks ago
228
    var supportsUMSettings: Bool {
Bogdan Timofte authored 2 weeks ago
229
        capabilities.supportsScreenSettings
Bogdan Timofte authored 2 weeks ago
230
    }
231

            
232
    var supportsRecordingThreshold: Bool {
Bogdan Timofte authored 2 weeks ago
233
        capabilities.supportsRecordingThreshold
Bogdan Timofte authored 2 weeks ago
234
    }
235

            
Bogdan Timofte authored 2 weeks ago
236
    var reportsCurrentScreenIndex: Bool {
237
        capabilities.reportsCurrentScreenIndex
238
    }
239

            
240
    var showsDataGroupEnergy: Bool {
241
        capabilities.showsDataGroupEnergy
242
    }
243

            
244
    var highlightsActiveDataGroup: Bool {
245
        if model == .TC66C {
246
            return hasObservedActiveDataGroup
247
        }
248
        return capabilities.highlightsActiveDataGroup
249
    }
250

            
Bogdan Timofte authored 2 weeks ago
251
    var supportsFahrenheit: Bool {
Bogdan Timofte authored 2 weeks ago
252
        capabilities.supportsFahrenheit
Bogdan Timofte authored 2 weeks ago
253
    }
254

            
Bogdan Timofte authored 2 weeks ago
255
    var supportsManualTemperatureUnitSelection: Bool {
256
        model == .TC66C
257
    }
258

            
Bogdan Timofte authored 2 weeks ago
259
    var supportsChargerDetection: Bool {
Bogdan Timofte authored 2 weeks ago
260
        capabilities.supportsChargerDetection
261
    }
262

            
Bogdan Timofte authored 2 weeks ago
263
    var dataGroupsTitle: String {
264
        capabilities.dataGroupsTitle
265
    }
266

            
Bogdan Timofte authored 2 weeks ago
267
    var documentedWorkingVoltage: String {
268
        capabilities.documentedWorkingVoltage
269
    }
270

            
Bogdan Timofte authored 2 weeks ago
271
    var chargerTypeDescription: String {
272
        capabilities.chargerTypeDescription(for: chargerTypeIndex)
Bogdan Timofte authored 2 weeks ago
273
    }
274

            
Bogdan Timofte authored 2 weeks ago
275
    var temperatureUnitDescription: String {
276
        if supportsManualTemperatureUnitSelection {
Bogdan Timofte authored 2 weeks ago
277
            return "Device-defined"
Bogdan Timofte authored 2 weeks ago
278
        }
Bogdan Timofte authored 2 weeks ago
279
        return systemTemperatureUnitPreference.localeTitle
Bogdan Timofte authored 2 weeks ago
280
    }
281

            
282
    var primaryTemperatureDescription: String {
Bogdan Timofte authored 2 weeks ago
283
        let value = displayedTemperatureValue.format(decimalDigits: 0)
Bogdan Timofte authored 2 weeks ago
284
        if supportsManualTemperatureUnitSelection {
Bogdan Timofte authored 2 weeks ago
285
            return "\(value)°"
Bogdan Timofte authored 2 weeks ago
286
        }
Bogdan Timofte authored 2 weeks ago
287
        return "\(value)\(systemTemperatureUnitPreference.symbol)"
Bogdan Timofte authored 2 weeks ago
288
    }
289

            
290
    var secondaryTemperatureDescription: String? {
Bogdan Timofte authored 2 weeks ago
291
        nil
292
    }
293

            
294
    var displayedTemperatureValue: Double {
295
        if supportsManualTemperatureUnitSelection {
296
            return temperatureCelsius
297
        }
298
        switch systemTemperatureUnitPreference {
299
        case .celsius:
300
            return displayedTemperatureCelsius
301
        case .fahrenheit:
302
            return displayedTemperatureFahrenheit
303
        }
304
    }
305

            
306
    private var displayedTemperatureCelsius: Double {
307
        if supportsManualTemperatureUnitSelection {
308
            switch tc66TemperatureUnitPreference {
309
            case .celsius:
310
                return temperatureCelsius
311
            case .fahrenheit:
312
                return (temperatureCelsius - 32) * 5 / 9
313
            }
314
        }
315
        return temperatureCelsius
316
    }
317

            
318
    private var displayedTemperatureFahrenheit: Double {
319
        if supportsManualTemperatureUnitSelection {
320
            switch tc66TemperatureUnitPreference {
321
            case .celsius:
322
                return (temperatureCelsius * 9 / 5) + 32
323
            case .fahrenheit:
324
                return temperatureCelsius
325
            }
326
        }
327
        if supportsFahrenheit, temperatureFahrenheit.isFinite {
328
            return temperatureFahrenheit
329
        }
330
        return (temperatureCelsius * 9 / 5) + 32
331
    }
332

            
333
    private var systemTemperatureUnitPreference: TemperatureUnitPreference {
334
        let locale = Locale.autoupdatingCurrent
335
        if #available(iOS 16.0, *) {
336
            switch locale.measurementSystem {
337
            case .us:
338
                return .fahrenheit
339
            default:
340
                return .celsius
341
            }
342
        }
343

            
344
        let regionCode = locale.regionCode ?? ""
345
        let fahrenheitRegions: Set<String> = ["US", "BS", "BZ", "KY", "PW", "LR", "FM", "MH"]
346
        return fahrenheitRegions.contains(regionCode) ? .fahrenheit : .celsius
Bogdan Timofte authored 2 weeks ago
347
    }
348

            
Bogdan Timofte authored 2 weeks ago
349
    var currentScreenDescription: String {
Bogdan Timofte authored 2 weeks ago
350
        guard reportsCurrentScreenIndex else {
351
            return "Page Controls"
352
        }
Bogdan Timofte authored 2 weeks ago
353
        if let label = capabilities.screenDescription(for: currentScreen) {
354
            return "Screen \(currentScreen): \(label)"
355
        }
356
        return "Screen \(currentScreen)"
357
    }
358

            
Bogdan Timofte authored 2 weeks ago
359
    var deviceModelName: String {
Bogdan Timofte authored 2 weeks ago
360
        if !reportedModelName.isEmpty {
361
            return reportedModelName
362
        }
363
        return model.canonicalName
Bogdan Timofte authored 2 weeks ago
364
    }
365

            
Bogdan Timofte authored 2 weeks ago
366
    var deviceModelSummary: String {
Bogdan Timofte authored 2 weeks ago
367
        let baseName = deviceModelName
Bogdan Timofte authored 2 weeks ago
368
        if modelNumber != 0 {
369
            return "\(baseName) (\(modelNumber))"
370
        }
371
        return baseName
372
    }
373

            
Bogdan Timofte authored 2 weeks ago
374
    var recordingDurationDescription: String {
375
        let totalSeconds = Int(recordingDuration)
376
        let hours = totalSeconds / 3600
377
        let minutes = (totalSeconds % 3600) / 60
378
        let seconds = totalSeconds % 60
379

            
380
        if hours > 0 {
381
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
382
        }
383
        return String(format: "%02d:%02d", minutes, seconds)
384
    }
385

            
Bogdan Timofte authored 2 weeks ago
386
    var chargeRecordDurationDescription: String {
387
        let totalSeconds = Int(chargeRecordDuration)
388
        let hours = totalSeconds / 3600
389
        let minutes = (totalSeconds % 3600) / 60
390
        let seconds = totalSeconds % 60
391

            
392
        if hours > 0 {
393
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
394
        }
395
        return String(format: "%02d:%02d", minutes, seconds)
396
    }
397

            
398
    var chargeRecordTimeRange: ClosedRange<Date>? {
399
        guard let start = chargeRecordStartTimestamp else { return nil }
400
        let end = chargeRecordEndTimestamp ?? chargeRecordLastTimestamp
401
        guard let end else { return nil }
402
        return start...end
403
    }
404

            
405
    var chargeRecordStatusText: String {
406
        switch chargeRecordState {
407
        case .waitingForStart:
408
            return "Waiting"
409
        case .active:
410
            return "Active"
411
        case .completed:
412
            return "Completed"
413
        }
414
    }
415

            
416
    var chargeRecordStatusColor: Color {
417
        switch chargeRecordState {
418
        case .waitingForStart:
419
            return .secondary
420
        case .active:
421
            return .red
422
        case .completed:
423
            return .green
424
        }
425
    }
426

            
Bogdan Timofte authored 2 weeks ago
427
    var dataGroupsHint: String? {
Bogdan Timofte authored 2 weeks ago
428
        if model == .TC66C {
429
            if hasObservedActiveDataGroup {
430
                return "The active memory is inferred from the totals that are currently increasing."
431
            }
432
            return "The device exposes two read-only memories. The active memory is inferred once one total starts increasing."
433
        }
434
        return capabilities.dataGroupsHint
435
    }
436

            
437
    func dataGroupLabel(for id: UInt8) -> String {
438
        supportsDataGroupCommands ? "\(id)" : "M\(Int(id) + 1)"
Bogdan Timofte authored 2 weeks ago
439
    }
440

            
441
    var recordingThresholdHint: String? {
442
        capabilities.recordingThresholdHint
443
    }
444

            
Bogdan Timofte authored 2 weeks ago
445
    var btSerial: BluetoothSerial
Bogdan Timofte authored 2 weeks ago
446

            
Bogdan Timofte authored 2 weeks ago
447
    var measurements = Measurements()
Bogdan Timofte authored 2 weeks ago
448

            
449
    private var commandQueue: [Data] = []
450
    private var dataDumpRequestTimestamp = Date()
Bogdan Timofte authored 2 weeks ago
451
    private var pendingDataDumpWorkItem: DispatchWorkItem?
Bogdan Timofte authored 2 weeks ago
452

            
453
    class DataGroupRecord {
Bogdan Timofte authored 2 weeks ago
454
        var ah: Double
455
        var wh: Double
Bogdan Timofte authored 2 weeks ago
456
        init(ah: Double, wh: Double) {
457
            self.ah = ah
458
            self.wh = wh
459
        }
460
    }
Bogdan Timofte authored 2 weeks ago
461
    private(set) var selectedDataGroup: UInt8 = 0
462
    private(set) var dataGroupRecords: [Int : DataGroupRecord] = [:]
463
    private(set) var chargeRecordAH: Double = 0
464
    private(set) var chargeRecordWH: Double = 0
465
    private(set) var chargeRecordDuration: TimeInterval = 0
Bogdan Timofte authored 2 weeks ago
466
    @Published var chargeRecordStopThreshold: Double = 0.05
467
    @Published var tc66TemperatureUnitPreference: TemperatureUnitPreference = .celsius {
468
        didSet {
469
            guard supportsManualTemperatureUnitSelection else { return }
470
            guard oldValue != tc66TemperatureUnitPreference else { return }
Bogdan Timofte authored a week ago
471
            appData.persistTC66TemperatureUnit(rawValue: tc66TemperatureUnitPreference.rawValue, for: btSerial.macAddress.description)
Bogdan Timofte authored 2 weeks ago
472
        }
473
    }
Bogdan Timofte authored 2 weeks ago
474

            
475
    @Published var screenBrightness: Int = -1 {
476
        didSet {
477
            if oldValue != screenBrightness {
478
                screenBrightnessTimestamp = Date()
479
                if oldValue != -1 {
480
                    setSceeenBrightness(to: UInt8(screenBrightness))
481
                }
482
            }
483
        }
484
    }
485
    private var screenBrightnessTimestamp = Date()
486

            
487
    @Published var screenTimeout: Int = -1 {
488
        didSet {
489
            if oldValue != screenTimeout {
490
                screenTimeoutTimestamp = Date()
491
                if oldValue != -1 {
492
                    setScreenSaverTimeout(to: UInt8(screenTimeout))
493
                }
494
            }
495
        }
496
    }
497
    private var screenTimeoutTimestamp = Date()
498

            
Bogdan Timofte authored 2 weeks ago
499
    private(set) var voltage: Double = 0
500
    private(set) var current: Double = 0
501
    private(set) var power: Double = 0
502
    private(set) var temperatureCelsius: Double = 0
503
    private(set) var temperatureFahrenheit: Double = 0
504
    private(set) var usbPlusVoltage: Double = 0
505
    private(set) var usbMinusVoltage: Double = 0
506
    private(set) var recordedAH: Double = 0
507
    private(set) var recordedWH: Double = 0
508
    private(set) var recording: Bool = false
Bogdan Timofte authored 2 weeks ago
509
    @Published var recordingTreshold: Double = 0 {
Bogdan Timofte authored 2 weeks ago
510
        didSet {
Bogdan Timofte authored 2 weeks ago
511
            guard recordingTreshold != oldValue else { return }
512
            if isApplyingRecordingThresholdFromDevice {
513
                return
Bogdan Timofte authored 2 weeks ago
514
            }
Bogdan Timofte authored 2 weeks ago
515
            recordingThresholdTimestamp = Date()
516
            guard recordingThresholdLoadedFromDevice else { return }
517
            setrecordingTreshold(to: (recordingTreshold * 100).uInt8Value)
Bogdan Timofte authored 2 weeks ago
518
        }
Bogdan Timofte authored 2 weeks ago
519
    }
Bogdan Timofte authored 2 weeks ago
520
    private(set) var currentScreen: UInt16 = 0
521
    private(set) var recordingDuration: UInt32 = 0
522
    private(set) var loadResistance: Double = 0
523
    private(set) var modelNumber: UInt16 = 0
524
    private(set) var chargerTypeIndex: UInt16 = 0
525
    private(set) var reportedModelName: String = ""
526
    private(set) var firmwareVersion: String = ""
527
    private(set) var serialNumber: UInt32 = 0
528
    private(set) var bootCount: UInt32 = 0
Bogdan Timofte authored 2 weeks ago
529
    private var enableAutoConnect: Bool = false
Bogdan Timofte authored 2 weeks ago
530
    private var recordingThresholdTimestamp = Date()
531
    private var recordingThresholdLoadedFromDevice = false
532
    private var isApplyingRecordingThresholdFromDevice = false
Bogdan Timofte authored 2 weeks ago
533
    private(set) var chargeRecordState = ChargeRecordState.waitingForStart
Bogdan Timofte authored 2 weeks ago
534
    private var chargeRecordStartTimestamp: Date?
535
    private var chargeRecordEndTimestamp: Date?
536
    private var chargeRecordLastTimestamp: Date?
537
    private var chargeRecordLastCurrent: Double = 0
538
    private var chargeRecordLastPower: Double = 0
Bogdan Timofte authored 2 weeks ago
539
    private let volatileMemoryDecreaseEpsilon = 0.0005
540
    private let initiatedVolatileMemoryResetGraceWindow: TimeInterval = 12
541
    private var hasSeenUMSnapshot = false
Bogdan Timofte authored 2 weeks ago
542
    private var hasObservedActiveDataGroup = false
543
    private var hasSeenTC66Snapshot = false
Bogdan Timofte authored 2 weeks ago
544
    private var pendingVolatileMemoryResetIgnoreCount = 0
545
    private var pendingVolatileMemoryResetDeadline: Date?
Bogdan Timofte authored 2 weeks ago
546
    private var liveDataChanged = false
Bogdan Timofte authored 2 weeks ago
547

            
Bogdan Timofte authored 2 weeks ago
548
    @discardableResult
549
    private func setIfChanged<T: Equatable>(_ keyPath: ReferenceWritableKeyPath<Meter, T>, to value: T) -> Bool {
550
        guard self[keyPath: keyPath] != value else { return false }
551
        self[keyPath: keyPath] = value
552
        liveDataChanged = true
553
        return true
554
    }
555

            
556
    private func updateDataGroupRecord(index: Int, ah: Double, wh: Double) {
557
        if let existing = dataGroupRecords[index] {
558
            if existing.ah != ah { existing.ah = ah; liveDataChanged = true }
559
            if existing.wh != wh { existing.wh = wh; liveDataChanged = true }
560
        } else {
561
            dataGroupRecords[index] = DataGroupRecord(ah: ah, wh: wh)
562
            liveDataChanged = true
563
        }
564
    }
565

            
Bogdan Timofte authored 2 weeks ago
566
    init ( model: Model, with serialPort: BluetoothSerial ) {
567
        uuid = serialPort.peripheral.identifier
568
        //dataStore.meterUUIDS.append(serialPort.peripheral.identifier)
Bogdan Timofte authored a week ago
569
        modelString = serialPort.peripheral.name ?? "Unknown Device"
Bogdan Timofte authored 2 weeks ago
570
        self.model = model
571
        btSerial = serialPort
Bogdan Timofte authored a week ago
572
        name = appData.persistedMeterName(for: serialPort.macAddress.description) ?? serialPort.macAddress.description
Bogdan Timofte authored 2 weeks ago
573
        super.init()
574
        btSerial.delegate = self
Bogdan Timofte authored 2 weeks ago
575
        reloadTemperatureUnitPreference()
Bogdan Timofte authored 2 weeks ago
576
        //name = dataStore.meterNames[macAddress.description] ?? peripheral.name!
577
        for index in stride(from: 0, through: 9, by: 1) {
578
            dataGroupRecords[index] = DataGroupRecord(ah:0, wh: 0)
579
        }
580
    }
Bogdan Timofte authored 2 weeks ago
581

            
582
    func reloadTemperatureUnitPreference() {
583
        guard supportsManualTemperatureUnitSelection else { return }
Bogdan Timofte authored a week ago
584
        let rawValue = appData.persistedTC66TemperatureUnitRawValue(for: btSerial.macAddress.description) ?? TemperatureUnitPreference.celsius.rawValue
Bogdan Timofte authored 2 weeks ago
585
        let persistedPreference = TemperatureUnitPreference(rawValue: rawValue) ?? .celsius
586
        if tc66TemperatureUnitPreference != persistedPreference {
587
            tc66TemperatureUnitPreference = persistedPreference
588
        }
589
    }
Bogdan Timofte authored 2 weeks ago
590

            
591
    private func cancelPendingDataDumpRequest(reason: String) {
592
        guard let pendingDataDumpWorkItem else { return }
593
        track("\(name) - Cancel scheduled data request (\(reason))")
594
        pendingDataDumpWorkItem.cancel()
595
        self.pendingDataDumpWorkItem = nil
596
    }
597

            
598
    private func scheduleDataDumpRequest(after delay: TimeInterval, reason: String) {
599
        cancelPendingDataDumpRequest(reason: "reschedule")
600

            
601
        let workItem = DispatchWorkItem { [weak self] in
602
            guard let self else { return }
603
            self.pendingDataDumpWorkItem = nil
604
            self.dataDumpRequest()
605
        }
606
        pendingDataDumpWorkItem = workItem
607
        track("\(name) - Schedule data request in \(String(format: "%.2f", delay))s (\(reason))")
608
        DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
609
    }
Bogdan Timofte authored 2 weeks ago
610

            
611
    private func noteInitiatedVolatileMemoryResetIfNeeded(for groupID: UInt8) {
612
        guard groupID == 0 else { return }
613
        pendingVolatileMemoryResetIgnoreCount += 1
614
        pendingVolatileMemoryResetDeadline = Date().addingTimeInterval(initiatedVolatileMemoryResetGraceWindow)
615
        track("\(name) - Will ignore the next volatile memory drop caused by an app reset request.")
616
    }
617

            
618
    private func pendingInitiatedVolatileMemoryResetIsActive(at timestamp: Date) -> Bool {
619
        guard let pendingVolatileMemoryResetDeadline else { return false }
620
        guard pendingVolatileMemoryResetIgnoreCount > 0 else {
621
            self.pendingVolatileMemoryResetDeadline = nil
622
            return false
623
        }
624
        guard timestamp <= pendingVolatileMemoryResetDeadline else {
625
            track("\(name) - Expiring stale volatile memory reset ignore state.")
626
            pendingVolatileMemoryResetIgnoreCount = 0
627
            self.pendingVolatileMemoryResetDeadline = nil
628
            return false
629
        }
630
        return true
631
    }
632

            
633
    private func shouldIgnoreVolatileMemoryDrop(at timestamp: Date) -> Bool {
634
        guard pendingInitiatedVolatileMemoryResetIsActive(at: timestamp) else { return false }
635
        pendingVolatileMemoryResetIgnoreCount -= 1
636
        if pendingVolatileMemoryResetIgnoreCount == 0 {
637
            pendingVolatileMemoryResetDeadline = nil
638
        }
639
        track("\(name) - Ignoring volatile memory drop after an app-initiated reset.")
640
        return true
641
    }
642

            
643
    private func didUMVolatileMemoryDecrease(in snapshot: UMSnapshot) -> Bool {
644
        guard hasSeenUMSnapshot else { return false }
645
        guard let previousRecord = dataGroupRecords[0], let nextRecord = snapshot.dataGroupRecords[0] else {
646
            return false
647
        }
648

            
649
        return nextRecord.ah < (previousRecord.ah - volatileMemoryDecreaseEpsilon)
650
            || nextRecord.wh < (previousRecord.wh - volatileMemoryDecreaseEpsilon)
651
    }
652

            
653
    private func didUMDeviceReboot(with snapshot: UMSnapshot, at timestamp: Date) -> Bool {
654
        defer { hasSeenUMSnapshot = true }
655

            
656
        guard didUMVolatileMemoryDecrease(in: snapshot) else { return false }
657
        guard !shouldIgnoreVolatileMemoryDrop(at: timestamp) else { return false }
658

            
659
        track("\(name) - Inferred UM reboot because volatile memory dropped.")
660
        return true
661
    }
662

            
663
    private func didTC66DeviceReboot(with snapshot: TC66Snapshot) -> Bool {
664
        guard hasSeenTC66Snapshot else { return false }
665
        guard snapshot.bootCount != bootCount else { return false }
666

            
667
        track("\(name) - Inferred TC66 reboot because bootCount changed from \(bootCount) to \(snapshot.bootCount).")
668
        return true
669
    }
670

            
671
    private func updateChargerTypeLatch(observedIndex: UInt16, didDetectDeviceReset: Bool) {
672
        if didDetectDeviceReset, chargerTypeIndex != 0 {
Bogdan Timofte authored 2 weeks ago
673
            setIfChanged(\.chargerTypeIndex, to: 0)
Bogdan Timofte authored 2 weeks ago
674
        }
675

            
676
        guard supportsChargerDetection else { return }
677

            
678
        if chargerTypeIndex == 0 {
Bogdan Timofte authored 2 weeks ago
679
            setIfChanged(\.chargerTypeIndex, to: observedIndex)
Bogdan Timofte authored 2 weeks ago
680
            return
681
        }
682

            
683
        guard observedIndex != 0, observedIndex != chargerTypeIndex else { return }
684
        track("\(name) - Ignoring charger type change from \(chargerTypeIndex) to \(observedIndex) until the device reboots.")
685
    }
Bogdan Timofte authored 2 weeks ago
686

            
687
    func dataDumpRequest() {
Bogdan Timofte authored 2 weeks ago
688
        guard operationalState >= .peripheralReady else {
689
            track("\(name) - Skip data request while state is \(operationalState)")
690
            return
691
        }
Bogdan Timofte authored 2 weeks ago
692
        if commandQueue.isEmpty {
693
            switch model {
694
            case .UM25C:
Bogdan Timofte authored 2 weeks ago
695
                btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
Bogdan Timofte authored 2 weeks ago
696
            case .UM34C:
Bogdan Timofte authored 2 weeks ago
697
                btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
Bogdan Timofte authored 2 weeks ago
698
            case .TC66C:
Bogdan Timofte authored 2 weeks ago
699
                btSerial.write(TC66Protocol.snapshotRequest, expectedResponseLength: 192)
Bogdan Timofte authored 2 weeks ago
700
            }
701
            dataDumpRequestTimestamp = Date()
702
            // track("\(name) - Request sent!")
703
        } else {
704
            track("Request delayed for: \(commandQueue.first!.hexEncodedStringValue)")
705
            btSerial.write( commandQueue.first! )
706
            commandQueue.removeFirst()
Bogdan Timofte authored 2 weeks ago
707
            scheduleDataDumpRequest(after: 1, reason: "queued command")
Bogdan Timofte authored 2 weeks ago
708
        }
709
    }
710

            
711
    /**
712
     received data parser
713
     - parameter buffer cotains response for data dump request
714
     - Decription metod for TC66C AES ECB response  found [here](https:github.com/krzyzanowskim/CryptoSwift/issues/693)
715
     */
716
    func parseData ( from buffer: Data) {
717
        //track("\(name)")
Bogdan Timofte authored 2 weeks ago
718
        liveDataChanged = false
Bogdan Timofte authored 2 weeks ago
719
        switch model {
720
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
721
            do {
722
                apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
723
            } catch {
724
                track("\(name) - Error: \(error)")
725
            }
Bogdan Timofte authored 2 weeks ago
726
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
727
            do {
728
                apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
729
            } catch {
730
                track("\(name) - Error: \(error)")
731
            }
Bogdan Timofte authored 2 weeks ago
732
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
733
            do {
734
                apply(tc66Snapshot: try TC66Protocol.parseSnapshot(from: buffer))
735
            } catch {
736
                track("\(name) - Error: \(error)")
737
            }
Bogdan Timofte authored 2 weeks ago
738
        }
Bogdan Timofte authored 2 weeks ago
739
        updateChargeRecord(at: dataDumpRequestTimestamp)
Bogdan Timofte authored 2 weeks ago
740
        measurements.addValues(timestamp: dataDumpRequestTimestamp, power: power, voltage: voltage, current: current)
741
//        DispatchQueue.global(qos: .userInitiated).asyncAfter( deadline: .now() + 0.33 ) {
742
//            //track("\(name) - Scheduled new request.")
743
//        }
Bogdan Timofte authored 2 weeks ago
744
        if operationalState != .dataIsAvailable {
745
            operationalState = .dataIsAvailable
746
        } else if liveDataChanged {
747
            objectWillChange.send()
748
        }
Bogdan Timofte authored 2 weeks ago
749
        dataDumpRequest()
750
    }
751

            
Bogdan Timofte authored 2 weeks ago
752
    private func apply(umSnapshot snapshot: UMSnapshot) {
Bogdan Timofte authored 2 weeks ago
753
        let didDetectDeviceReset = didUMDeviceReboot(with: snapshot, at: dataDumpRequestTimestamp)
Bogdan Timofte authored 2 weeks ago
754
        setIfChanged(\.modelNumber, to: snapshot.modelNumber)
755
        setIfChanged(\.voltage, to: snapshot.voltage)
756
        setIfChanged(\.current, to: snapshot.current)
757
        setIfChanged(\.power, to: snapshot.power)
758
        setIfChanged(\.temperatureCelsius, to: snapshot.temperatureCelsius)
759
        setIfChanged(\.temperatureFahrenheit, to: snapshot.temperatureFahrenheit)
760
        setIfChanged(\.selectedDataGroup, to: snapshot.selectedDataGroup)
Bogdan Timofte authored 2 weeks ago
761
        for (index, record) in snapshot.dataGroupRecords {
Bogdan Timofte authored 2 weeks ago
762
            updateDataGroupRecord(index: index, ah: record.ah, wh: record.wh)
Bogdan Timofte authored 2 weeks ago
763
        }
Bogdan Timofte authored 2 weeks ago
764
        setIfChanged(\.usbPlusVoltage, to: snapshot.usbPlusVoltage)
765
        setIfChanged(\.usbMinusVoltage, to: snapshot.usbMinusVoltage)
Bogdan Timofte authored 2 weeks ago
766
        updateChargerTypeLatch(observedIndex: snapshot.chargerTypeIndex, didDetectDeviceReset: didDetectDeviceReset)
Bogdan Timofte authored 2 weeks ago
767
        setIfChanged(\.recordedAH, to: snapshot.recordedAH)
768
        setIfChanged(\.recordedWH, to: snapshot.recordedWH)
Bogdan Timofte authored 2 weeks ago
769

            
770
        if recordingThresholdTimestamp < dataDumpRequestTimestamp || !recordingThresholdLoadedFromDevice {
771
            recordingThresholdLoadedFromDevice = true
772
            if recordingTreshold != snapshot.recordingThreshold {
773
                isApplyingRecordingThresholdFromDevice = true
774
                recordingTreshold = snapshot.recordingThreshold
775
                isApplyingRecordingThresholdFromDevice = false
776
            }
777
        } else {
778
            track("\(name) - Skip updating recordingThreshold (changed after request).")
779
        }
Bogdan Timofte authored 2 weeks ago
780
        setIfChanged(\.recordingDuration, to: snapshot.recordingDuration)
781
        setIfChanged(\.recording, to: snapshot.recording)
Bogdan Timofte authored 2 weeks ago
782

            
Bogdan Timofte authored 2 weeks ago
783
        if screenTimeoutTimestamp < dataDumpRequestTimestamp {
Bogdan Timofte authored 2 weeks ago
784
            if screenTimeout != snapshot.screenTimeout {
785
                screenTimeout = snapshot.screenTimeout
Bogdan Timofte authored 2 weeks ago
786
            }
787
        } else {
788
            track("\(name) - Skip updating screenTimeout (changed after request).")
789
        }
790

            
791
        if screenBrightnessTimestamp < dataDumpRequestTimestamp {
Bogdan Timofte authored 2 weeks ago
792
            if screenBrightness != snapshot.screenBrightness {
793
                screenBrightness = snapshot.screenBrightness
Bogdan Timofte authored 2 weeks ago
794
            }
795
        } else {
796
            track("\(name) - Skip updating screenBrightness (changed after request).")
797
        }
798

            
Bogdan Timofte authored 2 weeks ago
799
        setIfChanged(\.currentScreen, to: snapshot.currentScreen)
800
        setIfChanged(\.loadResistance, to: snapshot.loadResistance)
Bogdan Timofte authored 2 weeks ago
801
    }
802

            
Bogdan Timofte authored 2 weeks ago
803
    private func apply(tc66Snapshot snapshot: TC66Snapshot) {
Bogdan Timofte authored 2 weeks ago
804
        let didDetectDeviceReset = didTC66DeviceReboot(with: snapshot)
Bogdan Timofte authored 2 weeks ago
805
        if hasSeenTC66Snapshot {
806
            inferTC66ActiveDataGroup(from: snapshot)
807
        } else {
808
            hasSeenTC66Snapshot = true
809
        }
Bogdan Timofte authored 2 weeks ago
810
        setIfChanged(\.reportedModelName, to: snapshot.modelName)
811
        setIfChanged(\.firmwareVersion, to: snapshot.firmwareVersion)
812
        setIfChanged(\.serialNumber, to: snapshot.serialNumber)
813
        setIfChanged(\.bootCount, to: snapshot.bootCount)
Bogdan Timofte authored 2 weeks ago
814
        updateChargerTypeLatch(observedIndex: 0, didDetectDeviceReset: didDetectDeviceReset)
Bogdan Timofte authored 2 weeks ago
815
        setIfChanged(\.voltage, to: snapshot.voltage)
816
        setIfChanged(\.current, to: snapshot.current)
817
        setIfChanged(\.power, to: snapshot.power)
818
        setIfChanged(\.loadResistance, to: snapshot.loadResistance)
Bogdan Timofte authored 2 weeks ago
819
        for (index, record) in snapshot.dataGroupRecords {
Bogdan Timofte authored 2 weeks ago
820
            updateDataGroupRecord(index: index, ah: record.ah, wh: record.wh)
Bogdan Timofte authored 2 weeks ago
821
        }
Bogdan Timofte authored 2 weeks ago
822
        setIfChanged(\.temperatureCelsius, to: snapshot.temperatureCelsius)
823
        setIfChanged(\.usbPlusVoltage, to: snapshot.usbPlusVoltage)
824
        setIfChanged(\.usbMinusVoltage, to: snapshot.usbMinusVoltage)
Bogdan Timofte authored 2 weeks ago
825
    }
Bogdan Timofte authored 2 weeks ago
826

            
827
    private func inferTC66ActiveDataGroup(from snapshot: TC66Snapshot) {
828
        let candidate = snapshot.dataGroupRecords.compactMap { entry -> (UInt8, Double)? in
829
            let index = entry.key
830
            let record = entry.value
831
            guard let previous = dataGroupRecords[index] else { return nil }
832
            let deltaAH = max(record.ah - previous.ah, 0)
833
            let deltaWH = max(record.wh - previous.wh, 0)
834
            let score = deltaAH + deltaWH
835
            guard score > 0 else { return nil }
836
            return (UInt8(index), score)
837
        }
838
        .max { lhs, rhs in lhs.1 < rhs.1 }
839

            
840
        if let candidate {
841
            selectedDataGroup = candidate.0
842
            hasObservedActiveDataGroup = true
843
        }
844
    }
845

            
846
    private func updateChargeRecord(at timestamp: Date) {
847
        switch chargeRecordState {
848
        case .waitingForStart:
849
            guard current > chargeRecordStopThreshold else { return }
850
            chargeRecordState = .active
851
            chargeRecordStartTimestamp = timestamp
852
            chargeRecordEndTimestamp = timestamp
853
            chargeRecordLastTimestamp = timestamp
854
            chargeRecordLastCurrent = current
855
            chargeRecordLastPower = power
856
        case .active:
857
            if let lastTimestamp = chargeRecordLastTimestamp {
858
                let deltaSeconds = max(timestamp.timeIntervalSince(lastTimestamp), 0)
859
                chargeRecordAH += chargeRecordLastCurrent * deltaSeconds / 3600
860
                chargeRecordWH += chargeRecordLastPower * deltaSeconds / 3600
861
                chargeRecordDuration += deltaSeconds
862
            }
863
            chargeRecordEndTimestamp = timestamp
864
            chargeRecordLastTimestamp = timestamp
865
            chargeRecordLastCurrent = current
866
            chargeRecordLastPower = power
867
            if current <= chargeRecordStopThreshold {
868
                chargeRecordState = .completed
869
            }
870
        case .completed:
871
            break
872
        }
873
    }
874

            
875
    func resetChargeRecord() {
876
        chargeRecordAH = 0
877
        chargeRecordWH = 0
878
        chargeRecordDuration = 0
879
        chargeRecordState = .waitingForStart
880
        chargeRecordStartTimestamp = nil
881
        chargeRecordEndTimestamp = nil
882
        chargeRecordLastTimestamp = nil
883
        chargeRecordLastCurrent = 0
884
        chargeRecordLastPower = 0
Bogdan Timofte authored 2 weeks ago
885
        objectWillChange.send()
Bogdan Timofte authored 2 weeks ago
886
    }
887

            
888
    func resetChargeRecordGraph() {
889
        let cutoff = Date()
890
        resetChargeRecord()
891
        measurements.trim(before: cutoff)
892
    }
Bogdan Timofte authored 2 weeks ago
893

            
894
    func nextScreen() {
895
        switch model {
896
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
897
            commandQueue.append(UMProtocol.nextScreen)
Bogdan Timofte authored 2 weeks ago
898
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
899
            commandQueue.append(UMProtocol.nextScreen)
Bogdan Timofte authored 2 weeks ago
900
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
901
            commandQueue.append(TC66Protocol.nextPage)
Bogdan Timofte authored 2 weeks ago
902
        }
903
    }
904

            
905
    func rotateScreen() {
906
        switch model {
907
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
908
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
909
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
910
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
911
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
912
            commandQueue.append(TC66Protocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
913
        }
914
    }
915

            
916
    func previousScreen() {
917
        switch model {
918
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
919
            commandQueue.append(UMProtocol.previousScreen)
Bogdan Timofte authored 2 weeks ago
920
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
921
            commandQueue.append(UMProtocol.previousScreen)
Bogdan Timofte authored 2 weeks ago
922
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
923
            commandQueue.append(TC66Protocol.previousPage)
Bogdan Timofte authored 2 weeks ago
924
        }
925
    }
926

            
927
    func clear() {
Bogdan Timofte authored 2 weeks ago
928
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 weeks ago
929
        noteInitiatedVolatileMemoryResetIfNeeded(for: selectedDataGroup)
Bogdan Timofte authored 2 weeks ago
930
        commandQueue.append(UMProtocol.clearCurrentGroup)
Bogdan Timofte authored 2 weeks ago
931
    }
932

            
933
    func clear(group id: UInt8) {
Bogdan Timofte authored 2 weeks ago
934
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 weeks ago
935
        commandQueue.append(UMProtocol.selectDataGroup(id))
Bogdan Timofte authored 2 weeks ago
936
        noteInitiatedVolatileMemoryResetIfNeeded(for: id)
937
        commandQueue.append(UMProtocol.clearCurrentGroup)
Bogdan Timofte authored 2 weeks ago
938
        commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
Bogdan Timofte authored 2 weeks ago
939
    }
940

            
941
    func selectDataGroup ( id: UInt8) {
Bogdan Timofte authored 2 weeks ago
942
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 weeks ago
943
        track("\(name) - \(id)")
944
        selectedDataGroup = id
Bogdan Timofte authored 2 weeks ago
945
        objectWillChange.send()
Bogdan Timofte authored 2 weeks ago
946
        commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
Bogdan Timofte authored 2 weeks ago
947
    }
948

            
949
    private func setSceeenBrightness ( to value: UInt8) {
950
        track("\(name) - \(value)")
Bogdan Timofte authored 2 weeks ago
951
        guard supportsUMSettings else { return }
Bogdan Timofte authored 2 weeks ago
952
        commandQueue.append(UMProtocol.setScreenBrightness(value))
Bogdan Timofte authored 2 weeks ago
953
    }
954
    private func setScreenSaverTimeout ( to value: UInt8) {
955
        track("\(name) - \(value)")
Bogdan Timofte authored 2 weeks ago
956
        guard supportsUMSettings else { return }
Bogdan Timofte authored 2 weeks ago
957
        commandQueue.append(UMProtocol.setScreenSaverTimeout(value))
Bogdan Timofte authored 2 weeks ago
958
    }
959
    func setrecordingTreshold ( to value: UInt8) {
Bogdan Timofte authored 2 weeks ago
960
        guard supportsRecordingThreshold else { return }
Bogdan Timofte authored 2 weeks ago
961
        commandQueue.append(UMProtocol.setRecordingThreshold(value))
Bogdan Timofte authored 2 weeks ago
962
    }
963

            
964
    /**
965
     Connect to meter.
966
     1. It calls BluetoothSerial.connect
967
     */
968
    func connect() {
969
        enableAutoConnect = true
970
        btSerial.connect()
971
    }
972

            
973
    /**
974
     Disconnect from meter.
975
        It calls BluetoothSerial.disconnect
976
     */
977
    func disconnect() {
978
        enableAutoConnect = false
979
        btSerial.disconnect()
980
    }
981
}
982

            
983
extension Meter : SerialPortDelegate {
984

            
985
    func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
Bogdan Timofte authored 2 weeks ago
986
        let applyStateChange = {
987
            self.lastSeen = Date()
988
            switch serialPortOperationalState {
989
            case .peripheralNotConnected:
990
                self.operationalState = .peripheralNotConnected
991
            case .peripheralConnectionPending:
992
                self.operationalState = .peripheralConnectionPending
993
            case .peripheralConnected:
994
                self.operationalState = .peripheralConnected
995
            case .peripheralReady:
996
                self.operationalState = .peripheralReady
997
            }
998
        }
999

            
1000
        if Thread.isMainThread {
1001
            applyStateChange()
1002
        } else {
1003
            DispatchQueue.main.async(execute: applyStateChange)
Bogdan Timofte authored 2 weeks ago
1004
        }
1005
    }
1006

            
1007
    func didReceiveData(_ data: Data) {
Bogdan Timofte authored 2 weeks ago
1008
        let applyData = {
1009
            self.lastSeen = Date()
Bogdan Timofte authored 2 weeks ago
1010
            if self.operationalState < .comunicating {
1011
                self.operationalState = .comunicating
1012
            }
Bogdan Timofte authored 2 weeks ago
1013
            self.parseData(from: data)
1014
        }
1015

            
1016
        if Thread.isMainThread {
1017
            applyData()
1018
        } else {
1019
            DispatchQueue.main.async(execute: applyData)
1020
        }
Bogdan Timofte authored 2 weeks ago
1021
    }
1022
}