Newer Older
486 lines | 18.29kb
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

            
145
    @Published var btSerial: BluetoothSerial
146

            
147
    @Published var measurements = Measurements()
148

            
149
    private var commandQueue: [Data] = []
150
    private var dataDumpRequestTimestamp = Date()
151

            
152
    class DataGroupRecord {
153
        @Published var ah: Double
154
        @Published var wh: Double
155
        init(ah: Double, wh: Double) {
156
            self.ah = ah
157
            self.wh = wh
158
        }
159
    }
160
    @Published var selectedDataGroup: UInt8 = 0
161
    @Published var dataGroupRecords: [Int : DataGroupRecord] = [:]
162

            
163
    @Published var screenBrightness: Int = -1 {
164
        didSet {
165
            if oldValue != screenBrightness {
166
                screenBrightnessTimestamp = Date()
167
                if oldValue != -1 {
168
                    setSceeenBrightness(to: UInt8(screenBrightness))
169
                }
170
            }
171
        }
172
    }
173
    private var screenBrightnessTimestamp = Date()
174

            
175
    @Published var screenTimeout: Int = -1 {
176
        didSet {
177
            if oldValue != screenTimeout {
178
                screenTimeoutTimestamp = Date()
179
                if oldValue != -1 {
180
                    setScreenSaverTimeout(to: UInt8(screenTimeout))
181
                }
182
            }
183
        }
184
    }
185
    private var screenTimeoutTimestamp = Date()
186

            
187
    @Published var voltage: Double = 0
188
    @Published var current: Double = 0
189
    @Published var power: Double = 0
190
    @Published var temperatureCelsius: Double = 0
191
    @Published var temperatureFahrenheit: Double = 0
192
    @Published var usbPlusVoltage: Double = 0
193
    @Published var usbMinusVoltage: Double = 0
194
    @Published var recordedAH: Double = 0
195
    @Published var recordedWH: Double = 0
196
    @Published var recording: Bool = false
197
    @Published var recordingTreshold: Double = 0 /* MARK: Seteaza inutil la pornire {
198
        didSet {
199
            if recordingTreshold != oldValue {
200
                setrecordingTreshold(to: (recordingTreshold*100).uInt8Value)
201
            }
202
        }
203
    } */
204
    @Published var currentScreen: UInt16 = 0
205
    @Published var recordingDuration: UInt32 = 0
206
    @Published var loadResistance: Double = 0
207
    @Published var modelNumber: UInt16 = 0
208
    @Published var chargerTypeIndex: UInt16 = 0
209
    private var enableAutoConnect: Bool = false
210

            
211
    init ( model: Model, with serialPort: BluetoothSerial ) {
212
        uuid = serialPort.peripheral.identifier
213
        //dataStore.meterUUIDS.append(serialPort.peripheral.identifier)
214
        modelString = serialPort.peripheral.name!
215
        self.model = model
216
        btSerial = serialPort
217
        name = appData.meterNames[serialPort.macAddress.description] ?? serialPort.macAddress.description
218
        super.init()
219
        btSerial.delegate = self
220
        //name = dataStore.meterNames[macAddress.description] ?? peripheral.name!
221
        for index in stride(from: 0, through: 9, by: 1) {
222
            dataGroupRecords[index] = DataGroupRecord(ah:0, wh: 0)
223
        }
224
    }
225

            
226
    func dataDumpRequest() {
227
        if commandQueue.isEmpty {
228
            switch model {
229
            case .UM25C:
230
                btSerial.write( Data([0xF0]), expectedResponseLength: 130)
231
            case .UM34C:
232
                btSerial.write( Data([0xF0]), expectedResponseLength: 130)
233
            case .TC66C:
234
                btSerial.write( "bgetva\r\n".data(using: String.Encoding.ascii)!, expectedResponseLength: 192)
235
            }
236
            dataDumpRequestTimestamp = Date()
237
            // track("\(name) - Request sent!")
238
        } else {
239
            track("Request delayed for: \(commandQueue.first!.hexEncodedStringValue)")
240
            btSerial.write( commandQueue.first! )
241
            commandQueue.removeFirst()
242
            DispatchQueue.main.asyncAfter( deadline: .now() + 1 )  {
243
                self.dataDumpRequest()
244
            }
245
        }
246
    }
247

            
248
    /**
249
     received data parser
250
     - parameter buffer cotains response for data dump request
251
     - Decription metod for TC66C AES ECB response  found [here](https:github.com/krzyzanowskim/CryptoSwift/issues/693)
252
     */
253
    func parseData ( from buffer: Data) {
254
        //track("\(name)")
255
        switch model {
256
        case .UM25C:
257
            parseUMData(from: buffer)
258
        case .UM34C:
259
            parseUMData(from: buffer)
260
        case .TC66C:
261
            parseTCData( from: buffer )
262
        }
263
        measurements.addValues(timestamp: dataDumpRequestTimestamp, power: power, voltage: voltage, current: current)
264
//        DispatchQueue.global(qos: .userInitiated).asyncAfter( deadline: .now() + 0.33 ) {
265
//            //track("\(name) - Scheduled new request.")
266
//        }
267
        operationalState = .dataIsAvailable
268
        dataDumpRequest()
269
    }
270

            
271
    func parseUMData(from buffer: Data) {
272
        modelNumber = UInt16( bigEndian: buffer.value( from: 0 ) )
273
        switch model {
274
            case .UM25C:
275
                voltage = Double( UInt16( bigEndian: buffer.value( from: 2) ) )/1000
276
                current = Double( UInt16( bigEndian: buffer.value( from: 4) ) )/10000
277
            case .UM34C:
278
                voltage = Double( UInt16( bigEndian: buffer.value( from: 2) ) )/100
279
                current = Double( UInt16( bigEndian: buffer.value( from: 4) ) )/1000
280
            case .TC66C:
281
                track("\(name) - This is not possible!")
282

            
283
        }
284
        power = Double( UInt32( bigEndian: buffer.value( from: 6) ) )/1000
285
        temperatureCelsius = Double( UInt16( bigEndian: buffer.value( from: 10) ) )
286
        temperatureFahrenheit = Double( UInt16( bigEndian: buffer.value( from: 12) ) )
287
        selectedDataGroup = UInt8(UInt16( bigEndian: buffer.value( from: 14) ) )
288
        for index in stride(from: 0, through: 9, by: 1) {
289
            let offset = 16 + index * 8
290
            dataGroupRecords[index] = DataGroupRecord(ah: Double (UInt32( bigEndian: buffer.value( from: offset ) ) )/1000, wh:Double (UInt32( bigEndian: buffer.value( from: offset + 4 ) ) )/1000)
291
        }
292
        usbPlusVoltage = Double( UInt16( bigEndian: buffer.value( from: 96) ) )/100
293
        usbMinusVoltage = Double( UInt16( bigEndian: buffer.value( from: 98) ) )/100
294
        chargerTypeIndex = UInt16( bigEndian: buffer.value( from: 100) )
295
        recordedAH = Double (UInt32( bigEndian: buffer.value( from: 102 ) ) )/1000
296
        recordedWH = Double (UInt32( bigEndian: buffer.value( from: 106 ) ) )/1000
297
        recordingTreshold = Double (UInt16( bigEndian: buffer.value( from: 110 ) ) )/100
298
        recordingDuration = UInt32( bigEndian: buffer.value( from: 112 ) )
299
        recording = UInt16( bigEndian: buffer.value( from: 116 ) ) == 1
300
        if screenTimeoutTimestamp < dataDumpRequestTimestamp {
301
            let tmpValue = Int (UInt16( bigEndian: buffer.value( from: 118 ) ) )
302
            if screenTimeout != tmpValue {
303
                screenTimeout = tmpValue
304
            }
305
        } else {
306
            track("\(name) - Skip updating screenTimeout (changed after request).")
307
        }
308

            
309
        if screenBrightnessTimestamp < dataDumpRequestTimestamp {
310
            let tmpValue = Int (UInt16( bigEndian: buffer.value( from: 120 ) ) )
311
            if screenBrightness != tmpValue {
312
                screenBrightness = tmpValue
313
            }
314
        } else {
315
            track("\(name) - Skip updating screenBrightness (changed after request).")
316
        }
317

            
318
        currentScreen = UInt16(bigEndian: buffer.value( from: 126 ) )
319
        loadResistance = Double ( UInt32( bigEndian: buffer.value( from: 122 ) ) )/10
320
        //        track("\(name) - Model Number = \(modelNumber)")
321
        //        track("\(name) - chargerTypeIndex = \(chargerTypeIndex)")
322
    }
323

            
324
    private func validatePac ( id: UInt8, pac: Data ) -> Bool {
325
        //track("\(name) - \(id) - \(pac.hexEncodedStringValue)")
326
        let expectedHeader = "pac\(id)".data(using: String.Encoding.ascii)
327
        let pacHeader = pac.subdata(from: 0, length: 4)
328
        let expectedCRC = UInt16( bigEndian: pac.subdata(from: 0, length: 60).crc16(seed: 0xFFFF).value( from: 0 ) )
329
        let pacCRC = UInt16( littleEndian: pac.value(from: 60) )
330
        return expectedHeader == pacHeader && expectedCRC == pacCRC
331
    }
332

            
333
    func parseTCData(from buffer: Data) {
334
        do {
335
            let key: [UInt8] = [
336
                0x58, 0x21, 0xfa, 0x56, 0x01, 0xb2, 0xf0, 0x26,
337
                0x87, 0xff, 0x12, 0x04, 0x62, 0x2a, 0x4f, 0xb0,
338
                0x86, 0xf4, 0x02, 0x60, 0x81, 0x6f, 0x9a, 0x0b,
339
                0xa7, 0xf1, 0x06, 0x61, 0x9a, 0xb8, 0x72, 0x88
340
            ]
341
            let cipher = try! AES(key: key, blockMode: ECB())
342
            let decryptedBuffer = Data( try cipher.decrypt(buffer.bytes) )
343
                    let pac1: Data = decryptedBuffer.subdata( from: 0, length: 64 )
344
                    if validatePac(id: 1, pac: pac1) {
345
                        let pac2: Data = decryptedBuffer.subdata( from: 64, length: 64 )
346
                        if  validatePac(id: 2, pac: pac2) {
347
                            let pac3: Data = decryptedBuffer.subdata( from: 128, length: 64 )
348
                            if validatePac(id: 3, pac: pac3) {
349
            //                    let modelName = pac1.subdata(from: 4, length: 4).asciiString
350
            //                    track("\(name) - Model: \(modelName)")
351
            //                    let firmwareVersion = pac1.subdata(from: 8, length: 4).asciiString
352
            //                    track("\(name) - Firmware Version: \(firmwareVersion)")
353
            //                    let serialNumber = UInt32( littleEndian: pac1.value( from: 12 ) )
354
            //                    track("\(name) - Serial Number: \(serialNumber)")
355
            //                    let powerCycleCount = UInt32( littleEndian: pac1.value( from: 44 ) )
356
            //                    track("\(name) - Power Cycle Count: \(powerCycleCount)")
357
                                voltage = Double( UInt32( littleEndian: pac1.value( from: 48) ) )/10000
358
                                current = Double( UInt32( littleEndian: pac1.value( from: 52) ) )/100000
359
                                power = Double( UInt32( littleEndian: pac1.value( from: 56) ) )/10000
360
                                loadResistance = Double( UInt32( littleEndian: pac2.value( from: 4) ) )/10
361
                                for index in stride(from: 0, through: 1, by: 1) {
362
                                    let offset = 8 + index * 8
363
                                    dataGroupRecords[index] = DataGroupRecord(ah: Double (UInt32( littleEndian: pac2.value( from: offset ) ) )/1000, wh:Double (UInt32( littleEndian: pac2.value( from: offset + 40 ) ) )/1000)
364
                                }
365
                                temperatureCelsius = Double( UInt32( littleEndian: pac2.value( from: 28 ) )) * ( UInt32( littleEndian: pac2.value( from: 24 ) ) == 1 ? -1 : 1 )
366
                                usbPlusVoltage = Double( UInt32( littleEndian: pac2.value( from: 32) ) )/100
367
                                usbMinusVoltage = Double( UInt32( littleEndian: pac2.value( from: 36) ) )/100
368
                                return
369
                            }
370
                        }
371
                    }
372
                    track("\(name) - Invalid data")
373

            
374
        } catch {
375
            track("\(name) - Error: \(error)")
376
        }
377
    }
378

            
379
    func nextScreen() {
380
        switch model {
381
        case .UM25C:
382
            commandQueue.append( Data( [0xF1] ) )
383
        case .UM34C:
384
            commandQueue.append( Data( [0xF1] ) )
385
        case .TC66C:
386
            commandQueue.append( "bnextp\r\n".data(using: String.Encoding.ascii)! )
387
        }
388
    }
389

            
390
    func rotateScreen() {
391
        switch model {
392
        case .UM25C:
393
            commandQueue.append( Data( [0xF2] ) )
394
        case .UM34C:
395
            commandQueue.append( Data( [0xF2] ) )
396
        case .TC66C:
397
            commandQueue.append( "brotat\r\n".data(using: String.Encoding.ascii)! )
398
        }
399
    }
400

            
401
    func previousScreen() {
402
        switch model {
403
        case .UM25C:
404
            commandQueue.append( Data( [0xF3] ) )
405
        case .UM34C:
406
            commandQueue.append( Data( [0xF3] ) )
407
        case .TC66C:
408
            commandQueue.append( "blastp\r\n".data(using: String.Encoding.ascii)! )
409
        }
410
    }
411

            
412
    func clear() {
413
        guard model != .TC66C else { return }
414
        commandQueue.append( Data( [0xF4] ) )
415
    }
416

            
417
    func clear(group id: UInt8) {
418
        guard model != .TC66C else { return }
419
        commandQueue.append( Data( [0xA0 | id] ) )
420
        clear()
421
        commandQueue.append( Data( [0xA0 | selectedDataGroup] ) )
422
    }
423

            
424
    func selectDataGroup ( id: UInt8) {
425
        track("\(name) - \(id)")
426
        selectedDataGroup = id
427
        commandQueue.append( Data( [0xA0 | selectedDataGroup] ) )
428
    }
429

            
430
    private func setSceeenBrightness ( to value: UInt8) {
431
        track("\(name) - \(value)")
432
        guard model != .TC66C else { return }
433
        commandQueue.append( Data( [0xD0 | value] ) )
434
    }
435
    private func setScreenSaverTimeout ( to value: UInt8) {
436
        track("\(name) - \(value)")
437
        guard model != .TC66C else { return }
438
        commandQueue.append( Data( [0xE0 | value]) )
439
    }
440
    func setrecordingTreshold ( to value: UInt8) {
441
        guard model != .TC66C else { return }
442
        commandQueue.append( Data( [0xB0 + value] ) )
443
    }
444

            
445
    /**
446
     Connect to meter.
447
     1. It calls BluetoothSerial.connect
448
     */
449
    func connect() {
450
        enableAutoConnect = true
451
        btSerial.connect()
452
    }
453

            
454
    /**
455
     Disconnect from meter.
456
        It calls BluetoothSerial.disconnect
457
     */
458
    func disconnect() {
459
        enableAutoConnect = false
460
        btSerial.disconnect()
461
    }
462
}
463

            
464
extension Meter : SerialPortDelegate {
465

            
466
    func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
467
        lastSeen = Date()
468
        //track("\(name) - \(serialPortOperationalState)")
469
        switch serialPortOperationalState {
470
        case .peripheralNotConnected:
471
            operationalState = .peripheralNotConnected
472
        case .peripheralConnectionPending:
473
            operationalState = .peripheralConnectionPending
474
        case .peripheralConnected:
475
            operationalState = .peripheralConnected
476
        case .peripheralReady:
477
            operationalState = .peripheralReady
478
        }
479
    }
480

            
481
    func didReceiveData(_ data: Data) {
482
        lastSeen = Date()
483
        operationalState = .comunicating
484
        parseData(from: data)
485
    }
486
}