Newer Older
1019 lines | 36.147kb
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 {
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 weeks ago
100
            guard operationalState != oldValue else { return }
Bogdan Timofte authored 2 weeks ago
101
            if Self.shouldLogOperationalStateTransition(from: oldValue, to: operationalState) {
102
                track("\(name) - Operational state changed from \(oldValue) to \(operationalState)")
103
            }
Bogdan Timofte authored 2 weeks ago
104
            switch operationalState {
105
            case .notPresent:
Bogdan Timofte authored 2 weeks ago
106
                cancelPendingDataDumpRequest(reason: "meter missing")
Bogdan Timofte authored 2 weeks ago
107
                break
108
            case .peripheralNotConnected:
Bogdan Timofte authored 2 weeks ago
109
                cancelPendingDataDumpRequest(reason: "peripheral disconnected")
110
                if !commandQueue.isEmpty {
111
                    track("\(name) - Clearing \(commandQueue.count) queued commands after disconnect")
112
                    commandQueue.removeAll()
113
                }
Bogdan Timofte authored 2 weeks ago
114
                if enableAutoConnect {
115
                    track("\(name) - Reconnecting...")
116
                    btSerial.connect()
117
                }
118
            case .peripheralConnectionPending:
Bogdan Timofte authored 2 weeks ago
119
                cancelPendingDataDumpRequest(reason: "connection pending")
Bogdan Timofte authored 2 weeks ago
120
                break
121
            case .peripheralConnected:
Bogdan Timofte authored 2 weeks ago
122
                cancelPendingDataDumpRequest(reason: "services not ready yet")
Bogdan Timofte authored 2 weeks ago
123
                break
124
            case .peripheralReady:
Bogdan Timofte authored 2 weeks ago
125
                scheduleDataDumpRequest(after: 0.5, reason: "peripheral ready")
Bogdan Timofte authored 2 weeks ago
126
            case .comunicating:
127
                break
128
            case .dataIsAvailable:
129
                break
130
            }
131
        }
132
    }
133

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

            
153
    private var wdTimer: Timer?
154

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

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

            
Bogdan Timofte authored a week ago
175
    private var isSyncingNameFromStore = false
176

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

            
Bogdan Timofte authored a week ago
185
    var preferredTabIdentifier: String = "home"
186

            
Bogdan Timofte authored a week ago
187
    @Published private(set) var lastConnectedAt: Date?
Bogdan Timofte authored 2 weeks ago
188

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
Bogdan Timofte authored 2 weeks ago
428
    var btSerial: BluetoothSerial
Bogdan Timofte authored 2 weeks ago
429

            
Bogdan Timofte authored 2 weeks ago
430
    var measurements = Measurements()
Bogdan Timofte authored 2 weeks ago
431

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

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

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

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

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

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

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

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

            
567
    func reloadTemperatureUnitPreference() {
568
        guard supportsManualTemperatureUnitSelection else { return }
Bogdan Timofte authored a week ago
569
        let persistedPreference = appData.temperatureUnitPreference(for: btSerial.macAddress.description)
Bogdan Timofte authored 2 weeks ago
570
        if tc66TemperatureUnitPreference != persistedPreference {
571
            tc66TemperatureUnitPreference = persistedPreference
572
        }
573
    }
Bogdan Timofte authored 2 weeks ago
574

            
Bogdan Timofte authored a week ago
575
    func updateNameFromStore(_ newName: String) {
576
        guard newName != name else { return }
577
        isSyncingNameFromStore = true
578
        name = newName
579
        isSyncingNameFromStore = false
580
    }
581

            
Bogdan Timofte authored a week ago
582
    private func noteConnectionEstablished(at date: Date) {
583
        lastConnectedAt = date
584
        appData.noteMeterConnected(at: date, macAddress: btSerial.macAddress.description)
585
    }
586

            
Bogdan Timofte authored 2 weeks ago
587
    private func cancelPendingDataDumpRequest(reason: String) {
588
        guard let pendingDataDumpWorkItem else { return }
589
        track("\(name) - Cancel scheduled data request (\(reason))")
590
        pendingDataDumpWorkItem.cancel()
591
        self.pendingDataDumpWorkItem = nil
592
    }
593

            
594
    private func scheduleDataDumpRequest(after delay: TimeInterval, reason: String) {
595
        cancelPendingDataDumpRequest(reason: "reschedule")
596

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

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

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

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

            
639
    private func didUMVolatileMemoryDecrease(in snapshot: UMSnapshot) -> Bool {
640
        guard hasSeenUMSnapshot else { return false }
641
        guard let previousRecord = dataGroupRecords[0], let nextRecord = snapshot.dataGroupRecords[0] else {
642
            return false
643
        }
644

            
645
        return nextRecord.ah < (previousRecord.ah - volatileMemoryDecreaseEpsilon)
646
            || nextRecord.wh < (previousRecord.wh - volatileMemoryDecreaseEpsilon)
647
    }
648

            
649
    private func didUMDeviceReboot(with snapshot: UMSnapshot, at timestamp: Date) -> Bool {
650
        defer { hasSeenUMSnapshot = true }
651

            
652
        guard didUMVolatileMemoryDecrease(in: snapshot) else { return false }
653
        guard !shouldIgnoreVolatileMemoryDrop(at: timestamp) else { return false }
654

            
655
        track("\(name) - Inferred UM reboot because volatile memory dropped.")
656
        return true
657
    }
658

            
659
    private func didTC66DeviceReboot(with snapshot: TC66Snapshot) -> Bool {
660
        guard hasSeenTC66Snapshot else { return false }
661
        guard snapshot.bootCount != bootCount else { return false }
662

            
663
        track("\(name) - Inferred TC66 reboot because bootCount changed from \(bootCount) to \(snapshot.bootCount).")
664
        return true
665
    }
666

            
667
    private func updateChargerTypeLatch(observedIndex: UInt16, didDetectDeviceReset: Bool) {
668
        if didDetectDeviceReset, chargerTypeIndex != 0 {
Bogdan Timofte authored 2 weeks ago
669
            setIfChanged(\.chargerTypeIndex, to: 0)
Bogdan Timofte authored 2 weeks ago
670
        }
671

            
672
        guard supportsChargerDetection else { return }
673

            
674
        if chargerTypeIndex == 0 {
Bogdan Timofte authored 2 weeks ago
675
            setIfChanged(\.chargerTypeIndex, to: observedIndex)
Bogdan Timofte authored 2 weeks ago
676
            return
677
        }
678

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

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

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

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

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

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

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

            
Bogdan Timofte authored 2 weeks ago
795
        setIfChanged(\.currentScreen, to: snapshot.currentScreen)
796
        setIfChanged(\.loadResistance, to: snapshot.loadResistance)
Bogdan Timofte authored 2 weeks ago
797
    }
798

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

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

            
836
        if let candidate {
837
            selectedDataGroup = candidate.0
838
            hasObservedActiveDataGroup = true
839
        }
840
    }
841

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

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

            
884
    func resetChargeRecordGraph() {
885
        let cutoff = Date()
886
        resetChargeRecord()
887
        measurements.trim(before: cutoff)
888
    }
Bogdan Timofte authored 2 weeks ago
889

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

            
901
    func rotateScreen() {
902
        switch model {
903
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
904
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
905
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
906
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
907
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
908
            commandQueue.append(TC66Protocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
909
        }
910
    }
911

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

            
923
    func clear() {
Bogdan Timofte authored 2 weeks ago
924
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 weeks ago
925
        noteInitiatedVolatileMemoryResetIfNeeded(for: selectedDataGroup)
Bogdan Timofte authored 2 weeks ago
926
        commandQueue.append(UMProtocol.clearCurrentGroup)
Bogdan Timofte authored 2 weeks ago
927
    }
928

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

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

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

            
960
    /**
961
     Connect to meter.
962
     1. It calls BluetoothSerial.connect
963
     */
964
    func connect() {
965
        enableAutoConnect = true
966
        btSerial.connect()
967
    }
968

            
969
    /**
970
     Disconnect from meter.
971
        It calls BluetoothSerial.disconnect
972
     */
973
    func disconnect() {
974
        enableAutoConnect = false
975
        btSerial.disconnect()
976
    }
977
}
978

            
979
extension Meter : SerialPortDelegate {
980

            
981
    func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
Bogdan Timofte authored 2 weeks ago
982
        let applyStateChange = {
983
            self.lastSeen = Date()
984
            switch serialPortOperationalState {
985
            case .peripheralNotConnected:
986
                self.operationalState = .peripheralNotConnected
987
            case .peripheralConnectionPending:
988
                self.operationalState = .peripheralConnectionPending
989
            case .peripheralConnected:
Bogdan Timofte authored a week ago
990
                self.noteConnectionEstablished(at: Date())
Bogdan Timofte authored 2 weeks ago
991
                self.operationalState = .peripheralConnected
992
            case .peripheralReady:
993
                self.operationalState = .peripheralReady
994
            }
995
        }
996

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

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

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