USB-Meter / USB Meter / Model / BluetoothSerial.swift
Newer Older
385 lines | 14.88kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  bluetoothSerial.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 17/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8
//  https://github.com/hoiberg/HM10-BluetoothSerial-iOS
9
import CoreBluetooth
10

            
11
final class BluetoothSerial : NSObject, ObservableObject {
12

            
13
    enum AdministrativeState {
14
        case down
15
        case up
16
    }
17
    enum OperationalState: Int, Comparable {
18
        case peripheralNotConnected
19
        case peripheralConnectionPending
20
        case peripheralConnected
21
        case peripheralReady
22

            
23
        static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
24
            return lhs.rawValue < rhs.rawValue
25
        }
26
    }
27

            
28
    private var administrativeState = AdministrativeState.down
29
    private var operationalState = OperationalState.peripheralNotConnected {
30
        didSet {
31
            delegate?.opertionalStateChanged(to: operationalState)
32
        }
33
    }
34

            
35
    var macAddress: MACAddress
36
    private var manager: CBCentralManager
37
    private var radio: BluetoothRadio
Bogdan Timofte authored 2 weeks ago
38
    private(set) var rawRSSI: Int
39
    private var rssiSamples: [Int] = []
40
    private let rssiAveragingWindow = 3
41
    @Published private(set) var averageRSSI: Int
42
    private(set) var minRSSI: Int
43
    private(set) var maxRSSI: Int
Bogdan Timofte authored 2 weeks ago
44

            
45
    private var expectedResponseLength = 0
46
    private var wdTimer: Timer?
47

            
48
    var peripheral: CBPeripheral
49

            
Bogdan Timofte authored 2 weeks ago
50
    /// The characteristic used for writes on the connected peripheral.
Bogdan Timofte authored 2 weeks ago
51
    private var writeCharacteristic: CBCharacteristic?
Bogdan Timofte authored 2 weeks ago
52
    /// The characteristic used for notifications on the connected peripheral.
53
    private var notifyCharacteristic: CBCharacteristic?
Bogdan Timofte authored 2 weeks ago
54

            
55
    private var buffer = Data()
56

            
57
    weak var delegate: SerialPortDelegate?
58

            
59
    init( peripheral: CBPeripheral, radio: BluetoothRadio, with macAddress: MACAddress, managedBy manager: CBCentralManager, RSSI: Int ) {
60

            
61
        self.peripheral = peripheral
62
        self.macAddress = macAddress
63
        self.radio = radio
64
        self.manager = manager
Bogdan Timofte authored 2 weeks ago
65
        self.rawRSSI = RSSI
66
        self.rssiSamples = [RSSI]
67
        self.averageRSSI = RSSI
Bogdan Timofte authored 2 weeks ago
68
        self.minRSSI = RSSI
69
        self.maxRSSI = RSSI
Bogdan Timofte authored 2 weeks ago
70
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: {_ in
71
            if peripheral.state == .connected {
72
                peripheral.readRSSI()
73
            }
74
        })
75
        super.init()
76
        peripheral.delegate = self
77
    }
Bogdan Timofte authored 2 weeks ago
78

            
Bogdan Timofte authored 2 weeks ago
79
    func updateRSSI(_ value: Int) {
80
        rawRSSI = value
81
        rssiSamples.append(value)
82
        if rssiSamples.count > rssiAveragingWindow {
83
            rssiSamples.removeFirst()
84
        }
85
        let newAverage = rssiSamples.reduce(0, +) / rssiSamples.count
86
        minRSSI = Swift.min(minRSSI, newAverage)
87
        maxRSSI = Swift.max(maxRSSI, newAverage)
88
        if newAverage != averageRSSI {
89
            averageRSSI = newAverage
90
        }
91
    }
92

            
Bogdan Timofte authored 2 weeks ago
93
    private func resetCommunicationState(reason: String, clearCharacteristics: Bool) {
94
        if wdTimer != nil {
95
            track("Reset communication state (\(reason)) - invalidating watchdog")
96
        }
97
        wdTimer?.invalidate()
98
        wdTimer = nil
99

            
100
        if expectedResponseLength != 0 || !buffer.isEmpty {
101
            track("Reset communication state (\(reason)) - expected: \(expectedResponseLength), buffered: \(buffer.count)")
102
        }
103
        expectedResponseLength = 0
104
        buffer.removeAll()
105

            
106
        if clearCharacteristics {
107
            writeCharacteristic = nil
108
            notifyCharacteristic = nil
109
        }
110
    }
Bogdan Timofte authored 2 weeks ago
111

            
112
    func connect() {
113
        administrativeState = .up
114
        if operationalState < .peripheralConnected {
Bogdan Timofte authored 2 weeks ago
115
            resetCommunicationState(reason: "connect()", clearCharacteristics: true)
Bogdan Timofte authored 2 weeks ago
116
            operationalState = .peripheralConnectionPending
Bogdan Timofte authored 2 weeks ago
117
            track("Connect called for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
Bogdan Timofte authored 2 weeks ago
118
            manager.connect(peripheral, options: nil)
119
        } else {
120
            track("Peripheral allready connected: \(operationalState)")
121
        }
122
    }
123

            
124
    func disconnect() {
Bogdan Timofte authored 2 weeks ago
125
        administrativeState = .down
126
        resetCommunicationState(reason: "disconnect()", clearCharacteristics: true)
127
        if peripheral.state != .disconnected || operationalState != .peripheralNotConnected {
128
            track("Disconnect requested for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
Bogdan Timofte authored 2 weeks ago
129
            manager.cancelPeripheralConnection(peripheral)
130
        }
131
    }
132

            
133
    /**
134
     Send data
135

            
136
     - parameter data: Data to be sent.
137
     - parameter expectedResponseLength: Optional If message sent require a respnse the length for that response must be provideed. Incomming data will be buffered before calling delegate.didReceiveData
138
     */
139
    func write(_ data: Data, expectedResponseLength: Int = 0) {
140
        //track("\(self.expectedResponseLength)")
141
        //track(data.hexEncodedStringValue)
142
        guard operationalState == .peripheralReady else {
143
            track("Guard: \(operationalState)")
144
            return
145
        }
146
        guard self.expectedResponseLength == 0 else {
147
            track("Guard: \(self.expectedResponseLength)")
148
            return
149
        }
150

            
151
        self.expectedResponseLength = expectedResponseLength
152

            
153
//        track("Sending...")
Bogdan Timofte authored 2 weeks ago
154
        guard let writeCharacteristic else {
155
            track("Missing write characteristic for \(radio)")
156
            self.expectedResponseLength = 0
157
            return
Bogdan Timofte authored 2 weeks ago
158
        }
Bogdan Timofte authored 2 weeks ago
159

            
160
        let writeType: CBCharacteristicWriteType = writeCharacteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
161
        peripheral.writeValue(data, for: writeCharacteristic, type: writeType)
Bogdan Timofte authored 2 weeks ago
162
//        track("Sent!")
163
        if self.expectedResponseLength != 0 {
164
            setWDT()
165
        }
166
    }
167

            
168
    func connectionEstablished () {
Bogdan Timofte authored 2 weeks ago
169
        resetCommunicationState(reason: "connectionEstablished()", clearCharacteristics: true)
170
        track("Connection established for '\(peripheral.identifier)'")
Bogdan Timofte authored 2 weeks ago
171
        rssiSamples = [rawRSSI]
172
        averageRSSI = rawRSSI
173
        minRSSI = rawRSSI
174
        maxRSSI = rawRSSI
Bogdan Timofte authored 2 weeks ago
175
        operationalState = .peripheralConnected
176
        peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio])
177
    }
178

            
179
    func connectionClosed () {
Bogdan Timofte authored 2 weeks ago
180
        track("Connection closed for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
181
        resetCommunicationState(reason: "connectionClosed()", clearCharacteristics: true)
Bogdan Timofte authored 2 weeks ago
182
        rssiSamples = [rawRSSI]
183
        averageRSSI = rawRSSI
184
        minRSSI = rawRSSI
185
        maxRSSI = rawRSSI
Bogdan Timofte authored 2 weeks ago
186
        operationalState = .peripheralNotConnected
187
    }
188

            
189
    func setWDT() {
190
        wdTimer?.invalidate()
191
        wdTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {_ in
192
            track("Response timeout. Expected: \(self.expectedResponseLength) - buffer: \(self.buffer.count)")
193
            self.expectedResponseLength = 0
194
            self.disconnect()
195
        })
196
    }
197

            
Bogdan Timofte authored 2 weeks ago
198
    private func refreshOperationalStateIfReady() {
Bogdan Timofte authored 2 weeks ago
199
        guard let notifyCharacteristic, let writeCharacteristic else {
Bogdan Timofte authored 2 weeks ago
200
            return
201
        }
Bogdan Timofte authored 2 weeks ago
202
        guard notifyCharacteristic.isNotifying else {
203
            track("Waiting for notifications on '\(notifyCharacteristic.uuid)' before marking peripheral ready")
204
            return
205
        }
206
        track("Peripheral ready with notify '\(notifyCharacteristic.uuid)' and write '\(writeCharacteristic.uuid)'")
Bogdan Timofte authored 2 weeks ago
207
        operationalState = .peripheralReady
208
    }
209

            
210
    private func updateBT18Characteristics(for service: CBService) {
211
        for characteristic in service.characteristics ?? [] {
212
            switch characteristic.uuid {
213
            case CBUUID(string: "FFE1"):
214
                if characteristic.properties.contains(.notify) || characteristic.properties.contains(.indicate) {
215
                    peripheral.setNotifyValue(true, for: characteristic)
216
                    notifyCharacteristic = characteristic
217
                }
218
                if writeCharacteristic == nil &&
219
                    (characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse)) {
220
                    writeCharacteristic = characteristic
221
                }
222
            case CBUUID(string: "FFE2"):
223
                if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
224
                    // DX-BT18 documents FFE2 as the preferred write-only endpoint when present.
225
                    writeCharacteristic = characteristic
226
                }
227
            default:
228
                track ("Unexpected characteristic discovered: '\(characteristic)'")
229
            }
230
        }
231
        refreshOperationalStateIfReady()
232
    }
233

            
234
    private func updatePW0316Characteristics(for service: CBService) {
235
        for characteristic in service.characteristics ?? [] {
236
            switch characteristic.uuid {
237
            case CBUUID(string: "FFE9"): // TX from BLE side into UART
238
                writeCharacteristic = characteristic
239
            case CBUUID(string: "FFE4"): // RX notifications from UART side into BLE
240
                peripheral.setNotifyValue(true, for: characteristic)
241
                notifyCharacteristic = characteristic
242
            default:
243
                track ("Unexpected characteristic discovered: '\(characteristic)'")
244
            }
245
        }
246
        refreshOperationalStateIfReady()
247
    }
248

            
Bogdan Timofte authored 2 weeks ago
249
}
250

            
251
//  MARK:   CBPeripheralDelegate
252
extension BluetoothSerial : CBPeripheralDelegate {
253

            
254
    //  MARK:   didReadRSSI
255
    func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
256
        if error != nil {
257
            track( "Error: \(error!)" )
258
        }
Bogdan Timofte authored 2 weeks ago
259
        updateRSSI(RSSI.intValue)
Bogdan Timofte authored 2 weeks ago
260
    }
261

            
262
    //  MARK:   didDiscoverServices
263
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
264
        track("\(String(describing: peripheral.services))")
265
        if error != nil {
266
            track( "Error: \(error!)" )
267
        }
268
        switch radio {
269
        case .BT18:
270
            for service in peripheral.services! {
271
                switch service.uuid {
272
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
273
                    peripheral.discoverCharacteristics(Array(Set((BluetoothRadioNotifyUUIDs[radio] ?? []) + (BluetoothRadioWriteUUIDs[radio] ?? []))), for: service)
Bogdan Timofte authored 2 weeks ago
274
                default:
275
                    track ("Unexpected service discovered: '\(service)'")
276
                }
277
            }
278
        case .PW0316:
279
            for service in peripheral.services! {
280
                switch service.uuid {
281
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
282
                    peripheral.discoverCharacteristics(BluetoothRadioNotifyUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
283
                case CBUUID(string: "FFE5"):
Bogdan Timofte authored 2 weeks ago
284
                    peripheral.discoverCharacteristics(BluetoothRadioWriteUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
285
                default:
286
                    track ("Unexpected service discovered: '\(service)'")
287
                }
288
            }
289
        default:
290
            track("Radio \(radio) Not Implemented!")
291
        }
292
    }
293

            
294
    //  MARK:   didDiscoverCharacteristicsFor
295
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
296
        if error != nil {
297
            track( "Error: \(error!)" )
298
        }
299
        track("\(String(describing: service.characteristics))")
300
        switch radio {
301
        case .BT18:
Bogdan Timofte authored 2 weeks ago
302
            updateBT18Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
303
        case .PW0316:
Bogdan Timofte authored 2 weeks ago
304
            updatePW0316Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
305
        default:
306
            track("Radio \(radio) Not Implemented!")
307
        }
308
    }
Bogdan Timofte authored 2 weeks ago
309

            
310
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
311
        if error != nil {
312
            track("Error updating notification state for '\(characteristic.uuid)': \(error!)")
313
        }
314
        track("Notification state updated for '\(characteristic.uuid)' - isNotifying: \(characteristic.isNotifying)")
315
        if characteristic.uuid == notifyCharacteristic?.uuid {
316
            refreshOperationalStateIfReady()
317
        }
318
    }
Bogdan Timofte authored 2 weeks ago
319

            
320
    //  MARK:   didUpdateValueFor
321
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
322
//        track("")
323
        if error != nil {
324
            track( "Error: \(error!)" )
325
        }
Bogdan Timofte authored 2 weeks ago
326
        let incomingData = characteristic.value ?? Data()
327
        guard !incomingData.isEmpty else {
328
            track("Received empty update for '\(characteristic.uuid)'")
329
            return
330
        }
331
        guard expectedResponseLength > 0 else {
332
            if !buffer.isEmpty {
333
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' with residual buffer: \(buffer.count)")
334
                buffer.removeAll()
335
            } else {
336
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' while no response is expected")
337
            }
338
            return
339
        }
340

            
341
        let previousBufferCount = buffer.count
342
        buffer.append(incomingData)
Bogdan Timofte authored 2 weeks ago
343
//        track("\n\(buffer.hexEncodedStringValue)")
344
        switch buffer.count {
345
        case let x where x < expectedResponseLength:
346
            setWDT()
347
            //track("buffering")
348
            break;
349
        case let x where x == expectedResponseLength:
350
            //track("buffer ready")
351
            wdTimer?.invalidate()
352
            expectedResponseLength = 0
353
            delegate?.didReceiveData(buffer)
354
            buffer.removeAll()
355
        case let x where x > expectedResponseLength:
356
            // MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema
Bogdan Timofte authored 2 weeks ago
357
            let expectedLength = expectedResponseLength
358
            track("Buffer overflow for '\(characteristic.uuid)'. Chunk: \(incomingData.count), previous buffer: \(previousBufferCount), total: \(x), expected: \(expectedLength). Disconnecting to recover.")
359
            disconnect()
Bogdan Timofte authored 2 weeks ago
360
        default:
361
            track("This is not possible!")
362
        }
363
    }
364

            
365
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
Bogdan Timofte authored 2 weeks ago
366
        if let error = error { track( "Error: \(error)" ) }
Bogdan Timofte authored 2 weeks ago
367
    }
368

            
369
    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
370
        //track("")
371
    }
372

            
373
}
374

            
375
// MARK: SerialPortDelegate
376
protocol SerialPortDelegate: AnyObject {
377
    // MARK: State Changed
378
    func opertionalStateChanged( to newOperationalState: BluetoothSerial.OperationalState )
379
    // MARK: Data was received
380
    func didReceiveData(_ data: Data)
381
}
382

            
383
// MARK: SerialPortDelegate Optionals
384
extension SerialPortDelegate {
385
}