USB-Meter / USB Meter / Model / BluetoothSerial.swift
Newer Older
367 lines | 14.232kb
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
    @Published var RSSI: Int {
39
        didSet {
40
            minRSSI = Swift.min(minRSSI, RSSI)
41
            maxRSSI = Swift.max(maxRSSI, RSSI)
42
        }
43
    }
44
    @Published private(set) var minRSSI: Int
45
    @Published private(set) var maxRSSI: Int
Bogdan Timofte authored 2 weeks ago
46

            
47
    private var expectedResponseLength = 0
48
    private var wdTimer: Timer?
49

            
50
    var peripheral: CBPeripheral
51

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

            
57
    private var buffer = Data()
58

            
59
    weak var delegate: SerialPortDelegate?
60

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

            
63
        self.peripheral = peripheral
64
        self.macAddress = macAddress
65
        self.radio = radio
66
        self.manager = manager
67
        self.RSSI = 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

            
79
    private func resetCommunicationState(reason: String, clearCharacteristics: Bool) {
80
        if wdTimer != nil {
81
            track("Reset communication state (\(reason)) - invalidating watchdog")
82
        }
83
        wdTimer?.invalidate()
84
        wdTimer = nil
85

            
86
        if expectedResponseLength != 0 || !buffer.isEmpty {
87
            track("Reset communication state (\(reason)) - expected: \(expectedResponseLength), buffered: \(buffer.count)")
88
        }
89
        expectedResponseLength = 0
90
        buffer.removeAll()
91

            
92
        if clearCharacteristics {
93
            writeCharacteristic = nil
94
            notifyCharacteristic = nil
95
        }
96
    }
Bogdan Timofte authored 2 weeks ago
97

            
98
    func connect() {
99
        administrativeState = .up
100
        if operationalState < .peripheralConnected {
Bogdan Timofte authored 2 weeks ago
101
            resetCommunicationState(reason: "connect()", clearCharacteristics: true)
Bogdan Timofte authored 2 weeks ago
102
            operationalState = .peripheralConnectionPending
Bogdan Timofte authored 2 weeks ago
103
            track("Connect called for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
Bogdan Timofte authored 2 weeks ago
104
            manager.connect(peripheral, options: nil)
105
        } else {
106
            track("Peripheral allready connected: \(operationalState)")
107
        }
108
    }
109

            
110
    func disconnect() {
Bogdan Timofte authored 2 weeks ago
111
        administrativeState = .down
112
        resetCommunicationState(reason: "disconnect()", clearCharacteristics: true)
113
        if peripheral.state != .disconnected || operationalState != .peripheralNotConnected {
114
            track("Disconnect requested for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
Bogdan Timofte authored 2 weeks ago
115
            manager.cancelPeripheralConnection(peripheral)
116
        }
117
    }
118

            
119
    /**
120
     Send data
121

            
122
     - parameter data: Data to be sent.
123
     - 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
124
     */
125
    func write(_ data: Data, expectedResponseLength: Int = 0) {
126
        //track("\(self.expectedResponseLength)")
127
        //track(data.hexEncodedStringValue)
128
        guard operationalState == .peripheralReady else {
129
            track("Guard: \(operationalState)")
130
            return
131
        }
132
        guard self.expectedResponseLength == 0 else {
133
            track("Guard: \(self.expectedResponseLength)")
134
            return
135
        }
136

            
137
        self.expectedResponseLength = expectedResponseLength
138

            
139
//        track("Sending...")
Bogdan Timofte authored 2 weeks ago
140
        guard let writeCharacteristic else {
141
            track("Missing write characteristic for \(radio)")
142
            self.expectedResponseLength = 0
143
            return
Bogdan Timofte authored 2 weeks ago
144
        }
Bogdan Timofte authored 2 weeks ago
145

            
146
        let writeType: CBCharacteristicWriteType = writeCharacteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
147
        peripheral.writeValue(data, for: writeCharacteristic, type: writeType)
Bogdan Timofte authored 2 weeks ago
148
//        track("Sent!")
149
        if self.expectedResponseLength != 0 {
150
            setWDT()
151
        }
152
    }
153

            
154
    func connectionEstablished () {
Bogdan Timofte authored 2 weeks ago
155
        resetCommunicationState(reason: "connectionEstablished()", clearCharacteristics: true)
156
        track("Connection established for '\(peripheral.identifier)'")
Bogdan Timofte authored 2 weeks ago
157
        minRSSI = RSSI
158
        maxRSSI = RSSI
Bogdan Timofte authored 2 weeks ago
159
        operationalState = .peripheralConnected
160
        peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio])
161
    }
162

            
163
    func connectionClosed () {
Bogdan Timofte authored 2 weeks ago
164
        track("Connection closed for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
165
        resetCommunicationState(reason: "connectionClosed()", clearCharacteristics: true)
Bogdan Timofte authored 2 weeks ago
166
        minRSSI = RSSI
167
        maxRSSI = RSSI
Bogdan Timofte authored 2 weeks ago
168
        operationalState = .peripheralNotConnected
169
    }
170

            
171
    func setWDT() {
172
        wdTimer?.invalidate()
173
        wdTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {_ in
174
            track("Response timeout. Expected: \(self.expectedResponseLength) - buffer: \(self.buffer.count)")
175
            self.expectedResponseLength = 0
176
            self.disconnect()
177
        })
178
    }
179

            
Bogdan Timofte authored 2 weeks ago
180
    private func refreshOperationalStateIfReady() {
Bogdan Timofte authored 2 weeks ago
181
        guard let notifyCharacteristic, let writeCharacteristic else {
Bogdan Timofte authored 2 weeks ago
182
            return
183
        }
Bogdan Timofte authored 2 weeks ago
184
        guard notifyCharacteristic.isNotifying else {
185
            track("Waiting for notifications on '\(notifyCharacteristic.uuid)' before marking peripheral ready")
186
            return
187
        }
188
        track("Peripheral ready with notify '\(notifyCharacteristic.uuid)' and write '\(writeCharacteristic.uuid)'")
Bogdan Timofte authored 2 weeks ago
189
        operationalState = .peripheralReady
190
    }
191

            
192
    private func updateBT18Characteristics(for service: CBService) {
193
        for characteristic in service.characteristics ?? [] {
194
            switch characteristic.uuid {
195
            case CBUUID(string: "FFE1"):
196
                if characteristic.properties.contains(.notify) || characteristic.properties.contains(.indicate) {
197
                    peripheral.setNotifyValue(true, for: characteristic)
198
                    notifyCharacteristic = characteristic
199
                }
200
                if writeCharacteristic == nil &&
201
                    (characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse)) {
202
                    writeCharacteristic = characteristic
203
                }
204
            case CBUUID(string: "FFE2"):
205
                if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
206
                    // DX-BT18 documents FFE2 as the preferred write-only endpoint when present.
207
                    writeCharacteristic = characteristic
208
                }
209
            default:
210
                track ("Unexpected characteristic discovered: '\(characteristic)'")
211
            }
212
        }
213
        refreshOperationalStateIfReady()
214
    }
215

            
216
    private func updatePW0316Characteristics(for service: CBService) {
217
        for characteristic in service.characteristics ?? [] {
218
            switch characteristic.uuid {
219
            case CBUUID(string: "FFE9"): // TX from BLE side into UART
220
                writeCharacteristic = characteristic
221
            case CBUUID(string: "FFE4"): // RX notifications from UART side into BLE
222
                peripheral.setNotifyValue(true, for: characteristic)
223
                notifyCharacteristic = characteristic
224
            default:
225
                track ("Unexpected characteristic discovered: '\(characteristic)'")
226
            }
227
        }
228
        refreshOperationalStateIfReady()
229
    }
230

            
Bogdan Timofte authored 2 weeks ago
231
}
232

            
233
//  MARK:   CBPeripheralDelegate
234
extension BluetoothSerial : CBPeripheralDelegate {
235

            
236
    //  MARK:   didReadRSSI
237
    func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
238
        if error != nil {
239
            track( "Error: \(error!)" )
240
        }
241
        self.RSSI = RSSI.intValue
242
    }
243

            
244
    //  MARK:   didDiscoverServices
245
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
246
        track("\(String(describing: peripheral.services))")
247
        if error != nil {
248
            track( "Error: \(error!)" )
249
        }
250
        switch radio {
251
        case .BT18:
252
            for service in peripheral.services! {
253
                switch service.uuid {
254
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
255
                    peripheral.discoverCharacteristics(Array(Set((BluetoothRadioNotifyUUIDs[radio] ?? []) + (BluetoothRadioWriteUUIDs[radio] ?? []))), for: service)
Bogdan Timofte authored 2 weeks ago
256
                default:
257
                    track ("Unexpected service discovered: '\(service)'")
258
                }
259
            }
260
        case .PW0316:
261
            for service in peripheral.services! {
262
                switch service.uuid {
263
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
264
                    peripheral.discoverCharacteristics(BluetoothRadioNotifyUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
265
                case CBUUID(string: "FFE5"):
Bogdan Timofte authored 2 weeks ago
266
                    peripheral.discoverCharacteristics(BluetoothRadioWriteUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
267
                default:
268
                    track ("Unexpected service discovered: '\(service)'")
269
                }
270
            }
271
        default:
272
            track("Radio \(radio) Not Implemented!")
273
        }
274
    }
275

            
276
    //  MARK:   didDiscoverCharacteristicsFor
277
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
278
        if error != nil {
279
            track( "Error: \(error!)" )
280
        }
281
        track("\(String(describing: service.characteristics))")
282
        switch radio {
283
        case .BT18:
Bogdan Timofte authored 2 weeks ago
284
            updateBT18Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
285
        case .PW0316:
Bogdan Timofte authored 2 weeks ago
286
            updatePW0316Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
287
        default:
288
            track("Radio \(radio) Not Implemented!")
289
        }
290
    }
Bogdan Timofte authored 2 weeks ago
291

            
292
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
293
        if error != nil {
294
            track("Error updating notification state for '\(characteristic.uuid)': \(error!)")
295
        }
296
        track("Notification state updated for '\(characteristic.uuid)' - isNotifying: \(characteristic.isNotifying)")
297
        if characteristic.uuid == notifyCharacteristic?.uuid {
298
            refreshOperationalStateIfReady()
299
        }
300
    }
Bogdan Timofte authored 2 weeks ago
301

            
302
    //  MARK:   didUpdateValueFor
303
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
304
//        track("")
305
        if error != nil {
306
            track( "Error: \(error!)" )
307
        }
Bogdan Timofte authored 2 weeks ago
308
        let incomingData = characteristic.value ?? Data()
309
        guard !incomingData.isEmpty else {
310
            track("Received empty update for '\(characteristic.uuid)'")
311
            return
312
        }
313
        guard expectedResponseLength > 0 else {
314
            if !buffer.isEmpty {
315
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' with residual buffer: \(buffer.count)")
316
                buffer.removeAll()
317
            } else {
318
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' while no response is expected")
319
            }
320
            return
321
        }
322

            
323
        let previousBufferCount = buffer.count
324
        buffer.append(incomingData)
Bogdan Timofte authored 2 weeks ago
325
//        track("\n\(buffer.hexEncodedStringValue)")
326
        switch buffer.count {
327
        case let x where x < expectedResponseLength:
328
            setWDT()
329
            //track("buffering")
330
            break;
331
        case let x where x == expectedResponseLength:
332
            //track("buffer ready")
333
            wdTimer?.invalidate()
334
            expectedResponseLength = 0
335
            delegate?.didReceiveData(buffer)
336
            buffer.removeAll()
337
        case let x where x > expectedResponseLength:
338
            // MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema
Bogdan Timofte authored 2 weeks ago
339
            let expectedLength = expectedResponseLength
340
            track("Buffer overflow for '\(characteristic.uuid)'. Chunk: \(incomingData.count), previous buffer: \(previousBufferCount), total: \(x), expected: \(expectedLength). Disconnecting to recover.")
341
            disconnect()
Bogdan Timofte authored 2 weeks ago
342
        default:
343
            track("This is not possible!")
344
        }
345
    }
346

            
347
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
348
        if error != nil { track( "Error: \(error!)" ) }
349
    }
350

            
351
    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
352
        //track("")
353
    }
354

            
355
}
356

            
357
// MARK: SerialPortDelegate
358
protocol SerialPortDelegate: AnyObject {
359
    // MARK: State Changed
360
    func opertionalStateChanged( to newOperationalState: BluetoothSerial.OperationalState )
361
    // MARK: Data was received
362
    func didReceiveData(_ data: Data)
363
}
364

            
365
// MARK: SerialPortDelegate Optionals
366
extension SerialPortDelegate {
367
}