Newer Older
473 lines | 14.976kb
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 CryptoSwift
16
import SwiftUI
17

            
18
/**
19
 Supprted USB Meters
20
 # UM25C
21
 # TC66
22
 * Reverse Engineering
23
 [UM Series](https://sigrok.org/wiki/RDTech_UM_series)
24
 [TC66C](https://sigrok.org/wiki/RDTech_TC66C)
25
 */
26
enum Model {
27
    case UM25C
28
    case UM34C
29
    case TC66C
30
}
31

            
32
var modelRadios: [Model : BluetoothRadio] = [
33
    .UM25C : .BT18,
34
    .UM34C : .BT18,
35
    .TC66C : .PW0316
36
]
37

            
38
var ModelByPeriferalName: [String : Model] = [
39
    "UM25C" : .UM25C,
40
    "UM34C" : .UM34C,
41
    "TC66C" : .TC66C,
42
    "PW0316" : .TC66C
43
]
44

            
45
var colorForModel: [Model : Color] = [
46
    .UM25C : .blue,
47
    .UM34C : .yellow,
48
    .TC66C : .black
49
]
50

            
51

            
52
class Meter : NSObject, ObservableObject, Identifiable {
53

            
54
    enum OperationalState: Int, Comparable {
55
        case notPresent
56
        case peripheralNotConnected
57
        case peripheralConnectionPending
58
        case peripheralConnected
59
        case peripheralReady
60
        case comunicating
61
        case dataIsAvailable
62

            
63
        static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
64
            return lhs.rawValue < rhs.rawValue
65
        }
66
    }
67

            
68
    @Published var operationalState = OperationalState.peripheralNotConnected {
69
        didSet {
70
            switch operationalState {
71
            case .notPresent:
72
                break
73
            case .peripheralNotConnected:
74
                if enableAutoConnect {
75
                    track("\(name) - Reconnecting...")
76
                    btSerial.connect()
77
                }
78
            case .peripheralConnectionPending:
79
                break
80
            case .peripheralConnected:
81
                break
82
            case .peripheralReady:
83
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Change `2.0` to the desired number of seconds.
84
                    self.dataDumpRequest()
85
                }
86
            case .comunicating:
87
                break
88
            case .dataIsAvailable:
89
                break
90
            }
91
        }
92
    }
93

            
94
    static func operationalColor(for state: OperationalState) -> Color {
95
        switch state {
96
        case .notPresent:
97
            return .red
98
        case .peripheralNotConnected:
99
            return .blue
100
        case .peripheralConnectionPending:
101
            return .yellow
102
        case .peripheralConnected:
103
            return .yellow
104
        case .peripheralReady:
105
            return .orange
106
        case .comunicating:
107
            return .orange
108
        case .dataIsAvailable:
109
            return .green
110
        }
111
    }
112

            
113
    private var wdTimer: Timer?
114

            
115
    @Published var lastSeen = Date() {
116
        didSet {
117
            wdTimer?.invalidate()
118
            if operationalState == .peripheralNotConnected {
119
                wdTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false, block: {_ in
120
                    track("\(self.name) - Lost advertisments...")
121
                    self.operationalState = .notPresent
122
                })
123
            } else if operationalState == .notPresent {
124
               operationalState = .peripheralNotConnected
125
            }
126
        }
127
    }
128

            
129
    var uuid: UUID
130
    var model: Model
131
    var modelString: String
132

            
133
    var name: String {
134
        didSet {
135
            appData.meterNames[btSerial.macAddress.description] = name
136
        }
137
    }
138

            
139
    var color : Color {
140
        get {
141
            return colorForModel[model]!
142
        }
143
    }
144

            
Bogdan Timofte authored 2 weeks ago
145
    var availableDataGroupIDs: [UInt8] {
146
        switch model {
147
        case .UM25C, .UM34C:
148
            return Array(0...9)
149
        case .TC66C:
150
            return [0, 1]
151
        }
152
    }
153

            
154
    var supportsDataGroupCommands: Bool {
155
        model != .TC66C
156
    }
157

            
158
    var supportsUMSettings: Bool {
159
        model != .TC66C
160
    }
161

            
162
    var supportsRecordingThreshold: Bool {
163
        model != .TC66C
164
    }
165

            
166
    var supportsFahrenheit: Bool {
167
        model != .TC66C
168
    }
169

            
170
    var supportsChargerDetection: Bool {
171
        model != .TC66C
172
    }
173

            
Bogdan Timofte authored 2 weeks ago
174
    @Published var btSerial: BluetoothSerial
175

            
176
    @Published var measurements = Measurements()
177

            
178
    private var commandQueue: [Data] = []
179
    private var dataDumpRequestTimestamp = Date()
180

            
181
    class DataGroupRecord {
182
        @Published var ah: Double
183
        @Published var wh: Double
184
        init(ah: Double, wh: Double) {
185
            self.ah = ah
186
            self.wh = wh
187
        }
188
    }
189
    @Published var selectedDataGroup: UInt8 = 0
190
    @Published var dataGroupRecords: [Int : DataGroupRecord] = [:]
191

            
192
    @Published var screenBrightness: Int = -1 {
193
        didSet {
194
            if oldValue != screenBrightness {
195
                screenBrightnessTimestamp = Date()
196
                if oldValue != -1 {
197
                    setSceeenBrightness(to: UInt8(screenBrightness))
198
                }
199
            }
200
        }
201
    }
202
    private var screenBrightnessTimestamp = Date()
203

            
204
    @Published var screenTimeout: Int = -1 {
205
        didSet {
206
            if oldValue != screenTimeout {
207
                screenTimeoutTimestamp = Date()
208
                if oldValue != -1 {
209
                    setScreenSaverTimeout(to: UInt8(screenTimeout))
210
                }
211
            }
212
        }
213
    }
214
    private var screenTimeoutTimestamp = Date()
215

            
216
    @Published var voltage: Double = 0
217
    @Published var current: Double = 0
218
    @Published var power: Double = 0
219
    @Published var temperatureCelsius: Double = 0
220
    @Published var temperatureFahrenheit: Double = 0
221
    @Published var usbPlusVoltage: Double = 0
222
    @Published var usbMinusVoltage: Double = 0
223
    @Published var recordedAH: Double = 0
224
    @Published var recordedWH: Double = 0
225
    @Published var recording: Bool = false
226
    @Published var recordingTreshold: Double = 0 /* MARK: Seteaza inutil la pornire {
227
        didSet {
228
            if recordingTreshold != oldValue {
229
                setrecordingTreshold(to: (recordingTreshold*100).uInt8Value)
230
            }
231
        }
232
    } */
233
    @Published var currentScreen: UInt16 = 0
234
    @Published var recordingDuration: UInt32 = 0
235
    @Published var loadResistance: Double = 0
236
    @Published var modelNumber: UInt16 = 0
237
    @Published var chargerTypeIndex: UInt16 = 0
238
    private var enableAutoConnect: Bool = false
239

            
240
    init ( model: Model, with serialPort: BluetoothSerial ) {
241
        uuid = serialPort.peripheral.identifier
242
        //dataStore.meterUUIDS.append(serialPort.peripheral.identifier)
243
        modelString = serialPort.peripheral.name!
244
        self.model = model
245
        btSerial = serialPort
246
        name = appData.meterNames[serialPort.macAddress.description] ?? serialPort.macAddress.description
247
        super.init()
248
        btSerial.delegate = self
249
        //name = dataStore.meterNames[macAddress.description] ?? peripheral.name!
250
        for index in stride(from: 0, through: 9, by: 1) {
251
            dataGroupRecords[index] = DataGroupRecord(ah:0, wh: 0)
252
        }
253
    }
254

            
255
    func dataDumpRequest() {
256
        if commandQueue.isEmpty {
257
            switch model {
258
            case .UM25C:
Bogdan Timofte authored 2 weeks ago
259
                btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
Bogdan Timofte authored 2 weeks ago
260
            case .UM34C:
Bogdan Timofte authored 2 weeks ago
261
                btSerial.write(UMProtocol.snapshotRequest, expectedResponseLength: 130)
Bogdan Timofte authored 2 weeks ago
262
            case .TC66C:
Bogdan Timofte authored 2 weeks ago
263
                btSerial.write(TC66Protocol.snapshotRequest, expectedResponseLength: 192)
Bogdan Timofte authored 2 weeks ago
264
            }
265
            dataDumpRequestTimestamp = Date()
266
            // track("\(name) - Request sent!")
267
        } else {
268
            track("Request delayed for: \(commandQueue.first!.hexEncodedStringValue)")
269
            btSerial.write( commandQueue.first! )
270
            commandQueue.removeFirst()
271
            DispatchQueue.main.asyncAfter( deadline: .now() + 1 )  {
272
                self.dataDumpRequest()
273
            }
274
        }
275
    }
276

            
277
    /**
278
     received data parser
279
     - parameter buffer cotains response for data dump request
280
     - Decription metod for TC66C AES ECB response  found [here](https:github.com/krzyzanowskim/CryptoSwift/issues/693)
281
     */
282
    func parseData ( from buffer: Data) {
283
        //track("\(name)")
284
        switch model {
285
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
286
            do {
287
                apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
288
            } catch {
289
                track("\(name) - Error: \(error)")
290
            }
Bogdan Timofte authored 2 weeks ago
291
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
292
            do {
293
                apply(umSnapshot: try UMProtocol.parseSnapshot(from: buffer, model: model))
294
            } catch {
295
                track("\(name) - Error: \(error)")
296
            }
Bogdan Timofte authored 2 weeks ago
297
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
298
            do {
299
                apply(tc66Snapshot: try TC66Protocol.parseSnapshot(from: buffer))
300
            } catch {
301
                track("\(name) - Error: \(error)")
302
            }
Bogdan Timofte authored 2 weeks ago
303
        }
304
        measurements.addValues(timestamp: dataDumpRequestTimestamp, power: power, voltage: voltage, current: current)
305
//        DispatchQueue.global(qos: .userInitiated).asyncAfter( deadline: .now() + 0.33 ) {
306
//            //track("\(name) - Scheduled new request.")
307
//        }
308
        operationalState = .dataIsAvailable
309
        dataDumpRequest()
310
    }
311

            
Bogdan Timofte authored 2 weeks ago
312
    private func apply(umSnapshot snapshot: UMSnapshot) {
313
        modelNumber = snapshot.modelNumber
314
        voltage = snapshot.voltage
315
        current = snapshot.current
316
        power = snapshot.power
317
        temperatureCelsius = snapshot.temperatureCelsius
318
        temperatureFahrenheit = snapshot.temperatureFahrenheit
319
        selectedDataGroup = snapshot.selectedDataGroup
320
        for (index, record) in snapshot.dataGroupRecords {
321
            dataGroupRecords[index] = DataGroupRecord(ah: record.ah, wh: record.wh)
Bogdan Timofte authored 2 weeks ago
322
        }
Bogdan Timofte authored 2 weeks ago
323
        usbPlusVoltage = snapshot.usbPlusVoltage
324
        usbMinusVoltage = snapshot.usbMinusVoltage
325
        chargerTypeIndex = snapshot.chargerTypeIndex
326
        recordedAH = snapshot.recordedAH
327
        recordedWH = snapshot.recordedWH
328
        recordingTreshold = snapshot.recordingThreshold
329
        recordingDuration = snapshot.recordingDuration
330
        recording = snapshot.recording
331

            
Bogdan Timofte authored 2 weeks ago
332
        if screenTimeoutTimestamp < dataDumpRequestTimestamp {
Bogdan Timofte authored 2 weeks ago
333
            if screenTimeout != snapshot.screenTimeout {
334
                screenTimeout = snapshot.screenTimeout
Bogdan Timofte authored 2 weeks ago
335
            }
336
        } else {
337
            track("\(name) - Skip updating screenTimeout (changed after request).")
338
        }
339

            
340
        if screenBrightnessTimestamp < dataDumpRequestTimestamp {
Bogdan Timofte authored 2 weeks ago
341
            if screenBrightness != snapshot.screenBrightness {
342
                screenBrightness = snapshot.screenBrightness
Bogdan Timofte authored 2 weeks ago
343
            }
344
        } else {
345
            track("\(name) - Skip updating screenBrightness (changed after request).")
346
        }
347

            
Bogdan Timofte authored 2 weeks ago
348
        currentScreen = snapshot.currentScreen
349
        loadResistance = snapshot.loadResistance
Bogdan Timofte authored 2 weeks ago
350
    }
351

            
Bogdan Timofte authored 2 weeks ago
352
    private func apply(tc66Snapshot snapshot: TC66Snapshot) {
353
        voltage = snapshot.voltage
354
        current = snapshot.current
355
        power = snapshot.power
356
        loadResistance = snapshot.loadResistance
357
        for (index, record) in snapshot.dataGroupRecords {
358
            dataGroupRecords[index] = DataGroupRecord(ah: record.ah, wh: record.wh)
Bogdan Timofte authored 2 weeks ago
359
        }
Bogdan Timofte authored 2 weeks ago
360
        temperatureCelsius = snapshot.temperatureCelsius
361
        usbPlusVoltage = snapshot.usbPlusVoltage
362
        usbMinusVoltage = snapshot.usbMinusVoltage
Bogdan Timofte authored 2 weeks ago
363
    }
364

            
365
    func nextScreen() {
366
        switch model {
367
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
368
            commandQueue.append(UMProtocol.nextScreen)
Bogdan Timofte authored 2 weeks ago
369
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
370
            commandQueue.append(UMProtocol.nextScreen)
Bogdan Timofte authored 2 weeks ago
371
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
372
            commandQueue.append(TC66Protocol.nextPage)
Bogdan Timofte authored 2 weeks ago
373
        }
374
    }
375

            
376
    func rotateScreen() {
377
        switch model {
378
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
379
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
380
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
381
            commandQueue.append(UMProtocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
382
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
383
            commandQueue.append(TC66Protocol.rotateScreen)
Bogdan Timofte authored 2 weeks ago
384
        }
385
    }
386

            
387
    func previousScreen() {
388
        switch model {
389
        case .UM25C:
Bogdan Timofte authored 2 weeks ago
390
            commandQueue.append(UMProtocol.previousScreen)
Bogdan Timofte authored 2 weeks ago
391
        case .UM34C:
Bogdan Timofte authored 2 weeks ago
392
            commandQueue.append(UMProtocol.previousScreen)
Bogdan Timofte authored 2 weeks ago
393
        case .TC66C:
Bogdan Timofte authored 2 weeks ago
394
            commandQueue.append(TC66Protocol.previousPage)
Bogdan Timofte authored 2 weeks ago
395
        }
396
    }
397

            
398
    func clear() {
399
        guard model != .TC66C else { return }
Bogdan Timofte authored 2 weeks ago
400
        commandQueue.append(UMProtocol.clearCurrentGroup)
Bogdan Timofte authored 2 weeks ago
401
    }
402

            
403
    func clear(group id: UInt8) {
404
        guard model != .TC66C else { return }
Bogdan Timofte authored 2 weeks ago
405
        commandQueue.append(UMProtocol.selectDataGroup(id))
Bogdan Timofte authored 2 weeks ago
406
        clear()
Bogdan Timofte authored 2 weeks ago
407
        commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
Bogdan Timofte authored 2 weeks ago
408
    }
409

            
410
    func selectDataGroup ( id: UInt8) {
Bogdan Timofte authored 2 weeks ago
411
        guard supportsDataGroupCommands else { return }
Bogdan Timofte authored 2 weeks ago
412
        track("\(name) - \(id)")
413
        selectedDataGroup = id
Bogdan Timofte authored 2 weeks ago
414
        commandQueue.append(UMProtocol.selectDataGroup(selectedDataGroup))
Bogdan Timofte authored 2 weeks ago
415
    }
416

            
417
    private func setSceeenBrightness ( to value: UInt8) {
418
        track("\(name) - \(value)")
419
        guard model != .TC66C else { return }
Bogdan Timofte authored 2 weeks ago
420
        commandQueue.append(UMProtocol.setScreenBrightness(value))
Bogdan Timofte authored 2 weeks ago
421
    }
422
    private func setScreenSaverTimeout ( to value: UInt8) {
423
        track("\(name) - \(value)")
424
        guard model != .TC66C else { return }
Bogdan Timofte authored 2 weeks ago
425
        commandQueue.append(UMProtocol.setScreenSaverTimeout(value))
Bogdan Timofte authored 2 weeks ago
426
    }
427
    func setrecordingTreshold ( to value: UInt8) {
428
        guard model != .TC66C else { return }
Bogdan Timofte authored 2 weeks ago
429
        commandQueue.append(UMProtocol.setRecordingThreshold(value))
Bogdan Timofte authored 2 weeks ago
430
    }
431

            
432
    /**
433
     Connect to meter.
434
     1. It calls BluetoothSerial.connect
435
     */
436
    func connect() {
437
        enableAutoConnect = true
438
        btSerial.connect()
439
    }
440

            
441
    /**
442
     Disconnect from meter.
443
        It calls BluetoothSerial.disconnect
444
     */
445
    func disconnect() {
446
        enableAutoConnect = false
447
        btSerial.disconnect()
448
    }
449
}
450

            
451
extension Meter : SerialPortDelegate {
452

            
453
    func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
454
        lastSeen = Date()
455
        //track("\(name) - \(serialPortOperationalState)")
456
        switch serialPortOperationalState {
457
        case .peripheralNotConnected:
458
            operationalState = .peripheralNotConnected
459
        case .peripheralConnectionPending:
460
            operationalState = .peripheralConnectionPending
461
        case .peripheralConnected:
462
            operationalState = .peripheralConnected
463
        case .peripheralReady:
464
            operationalState = .peripheralReady
465
        }
466
    }
467

            
468
    func didReceiveData(_ data: Data) {
469
        lastSeen = Date()
470
        operationalState = .comunicating
471
        parseData(from: data)
472
    }
473
}