USB-Meter / USB Meter / Model / BluetoothSerial.swift
Newer Older
354 lines | 13.865kb
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
38
    @Published var RSSI: Int
39

            
40
    private var expectedResponseLength = 0
41
    private var wdTimer: Timer?
42

            
43
    var peripheral: CBPeripheral
44

            
Bogdan Timofte authored 2 weeks ago
45
    /// The characteristic used for writes on the connected peripheral.
Bogdan Timofte authored 2 weeks ago
46
    private var writeCharacteristic: CBCharacteristic?
Bogdan Timofte authored 2 weeks ago
47
    /// The characteristic used for notifications on the connected peripheral.
48
    private var notifyCharacteristic: CBCharacteristic?
Bogdan Timofte authored 2 weeks ago
49

            
50
    private var buffer = Data()
51

            
52
    weak var delegate: SerialPortDelegate?
53

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

            
56
        self.peripheral = peripheral
57
        self.macAddress = macAddress
58
        self.radio = radio
59
        self.manager = manager
60
        self.RSSI = RSSI
61
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: {_ in
62
            if peripheral.state == .connected {
63
                peripheral.readRSSI()
64
            }
65
        })
66
        super.init()
67
        peripheral.delegate = self
68
    }
Bogdan Timofte authored 2 weeks ago
69

            
70
    private func resetCommunicationState(reason: String, clearCharacteristics: Bool) {
71
        if wdTimer != nil {
72
            track("Reset communication state (\(reason)) - invalidating watchdog")
73
        }
74
        wdTimer?.invalidate()
75
        wdTimer = nil
76

            
77
        if expectedResponseLength != 0 || !buffer.isEmpty {
78
            track("Reset communication state (\(reason)) - expected: \(expectedResponseLength), buffered: \(buffer.count)")
79
        }
80
        expectedResponseLength = 0
81
        buffer.removeAll()
82

            
83
        if clearCharacteristics {
84
            writeCharacteristic = nil
85
            notifyCharacteristic = nil
86
        }
87
    }
Bogdan Timofte authored 2 weeks ago
88

            
89
    func connect() {
90
        administrativeState = .up
91
        if operationalState < .peripheralConnected {
Bogdan Timofte authored 2 weeks ago
92
            resetCommunicationState(reason: "connect()", clearCharacteristics: true)
Bogdan Timofte authored 2 weeks ago
93
            operationalState = .peripheralConnectionPending
Bogdan Timofte authored 2 weeks ago
94
            track("Connect called for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
Bogdan Timofte authored 2 weeks ago
95
            manager.connect(peripheral, options: nil)
96
        } else {
97
            track("Peripheral allready connected: \(operationalState)")
98
        }
99
    }
100

            
101
    func disconnect() {
Bogdan Timofte authored 2 weeks ago
102
        administrativeState = .down
103
        resetCommunicationState(reason: "disconnect()", clearCharacteristics: true)
104
        if peripheral.state != .disconnected || operationalState != .peripheralNotConnected {
105
            track("Disconnect requested for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
Bogdan Timofte authored 2 weeks ago
106
            manager.cancelPeripheralConnection(peripheral)
107
        }
108
    }
109

            
110
    /**
111
     Send data
112

            
113
     - parameter data: Data to be sent.
114
     - 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
115
     */
116
    func write(_ data: Data, expectedResponseLength: Int = 0) {
117
        //track("\(self.expectedResponseLength)")
118
        //track(data.hexEncodedStringValue)
119
        guard operationalState == .peripheralReady else {
120
            track("Guard: \(operationalState)")
121
            return
122
        }
123
        guard self.expectedResponseLength == 0 else {
124
            track("Guard: \(self.expectedResponseLength)")
125
            return
126
        }
127

            
128
        self.expectedResponseLength = expectedResponseLength
129

            
130
//        track("Sending...")
Bogdan Timofte authored 2 weeks ago
131
        guard let writeCharacteristic else {
132
            track("Missing write characteristic for \(radio)")
133
            self.expectedResponseLength = 0
134
            return
Bogdan Timofte authored 2 weeks ago
135
        }
Bogdan Timofte authored 2 weeks ago
136

            
137
        let writeType: CBCharacteristicWriteType = writeCharacteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
138
        peripheral.writeValue(data, for: writeCharacteristic, type: writeType)
Bogdan Timofte authored 2 weeks ago
139
//        track("Sent!")
140
        if self.expectedResponseLength != 0 {
141
            setWDT()
142
        }
143
    }
144

            
145
    func connectionEstablished () {
Bogdan Timofte authored 2 weeks ago
146
        resetCommunicationState(reason: "connectionEstablished()", clearCharacteristics: true)
147
        track("Connection established for '\(peripheral.identifier)'")
Bogdan Timofte authored 2 weeks ago
148
        operationalState = .peripheralConnected
149
        peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio])
150
    }
151

            
152
    func connectionClosed () {
Bogdan Timofte authored 2 weeks ago
153
        track("Connection closed for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
154
        resetCommunicationState(reason: "connectionClosed()", clearCharacteristics: true)
Bogdan Timofte authored 2 weeks ago
155
        operationalState = .peripheralNotConnected
156
    }
157

            
158
    func setWDT() {
159
        wdTimer?.invalidate()
160
        wdTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {_ in
161
            track("Response timeout. Expected: \(self.expectedResponseLength) - buffer: \(self.buffer.count)")
162
            self.expectedResponseLength = 0
163
            self.disconnect()
164
        })
165
    }
166

            
Bogdan Timofte authored 2 weeks ago
167
    private func refreshOperationalStateIfReady() {
Bogdan Timofte authored 2 weeks ago
168
        guard let notifyCharacteristic, let writeCharacteristic else {
Bogdan Timofte authored 2 weeks ago
169
            return
170
        }
Bogdan Timofte authored 2 weeks ago
171
        guard notifyCharacteristic.isNotifying else {
172
            track("Waiting for notifications on '\(notifyCharacteristic.uuid)' before marking peripheral ready")
173
            return
174
        }
175
        track("Peripheral ready with notify '\(notifyCharacteristic.uuid)' and write '\(writeCharacteristic.uuid)'")
Bogdan Timofte authored 2 weeks ago
176
        operationalState = .peripheralReady
177
    }
178

            
179
    private func updateBT18Characteristics(for service: CBService) {
180
        for characteristic in service.characteristics ?? [] {
181
            switch characteristic.uuid {
182
            case CBUUID(string: "FFE1"):
183
                if characteristic.properties.contains(.notify) || characteristic.properties.contains(.indicate) {
184
                    peripheral.setNotifyValue(true, for: characteristic)
185
                    notifyCharacteristic = characteristic
186
                }
187
                if writeCharacteristic == nil &&
188
                    (characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse)) {
189
                    writeCharacteristic = characteristic
190
                }
191
            case CBUUID(string: "FFE2"):
192
                if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
193
                    // DX-BT18 documents FFE2 as the preferred write-only endpoint when present.
194
                    writeCharacteristic = characteristic
195
                }
196
            default:
197
                track ("Unexpected characteristic discovered: '\(characteristic)'")
198
            }
199
        }
200
        refreshOperationalStateIfReady()
201
    }
202

            
203
    private func updatePW0316Characteristics(for service: CBService) {
204
        for characteristic in service.characteristics ?? [] {
205
            switch characteristic.uuid {
206
            case CBUUID(string: "FFE9"): // TX from BLE side into UART
207
                writeCharacteristic = characteristic
208
            case CBUUID(string: "FFE4"): // RX notifications from UART side into BLE
209
                peripheral.setNotifyValue(true, for: characteristic)
210
                notifyCharacteristic = characteristic
211
            default:
212
                track ("Unexpected characteristic discovered: '\(characteristic)'")
213
            }
214
        }
215
        refreshOperationalStateIfReady()
216
    }
217

            
Bogdan Timofte authored 2 weeks ago
218
}
219

            
220
//  MARK:   CBPeripheralDelegate
221
extension BluetoothSerial : CBPeripheralDelegate {
222

            
223
    //  MARK:   didReadRSSI
224
    func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
225
        if error != nil {
226
            track( "Error: \(error!)" )
227
        }
228
        self.RSSI = RSSI.intValue
229
    }
230

            
231
    //  MARK:   didDiscoverServices
232
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
233
        track("\(String(describing: peripheral.services))")
234
        if error != nil {
235
            track( "Error: \(error!)" )
236
        }
237
        switch radio {
238
        case .BT18:
239
            for service in peripheral.services! {
240
                switch service.uuid {
241
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
242
                    peripheral.discoverCharacteristics(Array(Set((BluetoothRadioNotifyUUIDs[radio] ?? []) + (BluetoothRadioWriteUUIDs[radio] ?? []))), for: service)
Bogdan Timofte authored 2 weeks ago
243
                default:
244
                    track ("Unexpected service discovered: '\(service)'")
245
                }
246
            }
247
        case .PW0316:
248
            for service in peripheral.services! {
249
                switch service.uuid {
250
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
251
                    peripheral.discoverCharacteristics(BluetoothRadioNotifyUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
252
                case CBUUID(string: "FFE5"):
Bogdan Timofte authored 2 weeks ago
253
                    peripheral.discoverCharacteristics(BluetoothRadioWriteUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
254
                default:
255
                    track ("Unexpected service discovered: '\(service)'")
256
                }
257
            }
258
        default:
259
            track("Radio \(radio) Not Implemented!")
260
        }
261
    }
262

            
263
    //  MARK:   didDiscoverCharacteristicsFor
264
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
265
        if error != nil {
266
            track( "Error: \(error!)" )
267
        }
268
        track("\(String(describing: service.characteristics))")
269
        switch radio {
270
        case .BT18:
Bogdan Timofte authored 2 weeks ago
271
            updateBT18Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
272
        case .PW0316:
Bogdan Timofte authored 2 weeks ago
273
            updatePW0316Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
274
        default:
275
            track("Radio \(radio) Not Implemented!")
276
        }
277
    }
Bogdan Timofte authored 2 weeks ago
278

            
279
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
280
        if error != nil {
281
            track("Error updating notification state for '\(characteristic.uuid)': \(error!)")
282
        }
283
        track("Notification state updated for '\(characteristic.uuid)' - isNotifying: \(characteristic.isNotifying)")
284
        if characteristic.uuid == notifyCharacteristic?.uuid {
285
            refreshOperationalStateIfReady()
286
        }
287
    }
Bogdan Timofte authored 2 weeks ago
288

            
289
    //  MARK:   didUpdateValueFor
290
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
291
//        track("")
292
        if error != nil {
293
            track( "Error: \(error!)" )
294
        }
Bogdan Timofte authored 2 weeks ago
295
        let incomingData = characteristic.value ?? Data()
296
        guard !incomingData.isEmpty else {
297
            track("Received empty update for '\(characteristic.uuid)'")
298
            return
299
        }
300
        guard expectedResponseLength > 0 else {
301
            if !buffer.isEmpty {
302
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' with residual buffer: \(buffer.count)")
303
                buffer.removeAll()
304
            } else {
305
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' while no response is expected")
306
            }
307
            return
308
        }
309

            
310
        let previousBufferCount = buffer.count
311
        buffer.append(incomingData)
Bogdan Timofte authored 2 weeks ago
312
//        track("\n\(buffer.hexEncodedStringValue)")
313
        switch buffer.count {
314
        case let x where x < expectedResponseLength:
315
            setWDT()
316
            //track("buffering")
317
            break;
318
        case let x where x == expectedResponseLength:
319
            //track("buffer ready")
320
            wdTimer?.invalidate()
321
            expectedResponseLength = 0
322
            delegate?.didReceiveData(buffer)
323
            buffer.removeAll()
324
        case let x where x > expectedResponseLength:
325
            // MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema
Bogdan Timofte authored 2 weeks ago
326
            let expectedLength = expectedResponseLength
327
            track("Buffer overflow for '\(characteristic.uuid)'. Chunk: \(incomingData.count), previous buffer: \(previousBufferCount), total: \(x), expected: \(expectedLength). Disconnecting to recover.")
328
            disconnect()
Bogdan Timofte authored 2 weeks ago
329
        default:
330
            track("This is not possible!")
331
        }
332
    }
333

            
334
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
335
        if error != nil { track( "Error: \(error!)" ) }
336
    }
337

            
338
    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
339
        //track("")
340
    }
341

            
342
}
343

            
344
// MARK: SerialPortDelegate
345
protocol SerialPortDelegate: AnyObject {
346
    // MARK: State Changed
347
    func opertionalStateChanged( to newOperationalState: BluetoothSerial.OperationalState )
348
    // MARK: Data was received
349
    func didReceiveData(_ data: Data)
350
}
351

            
352
// MARK: SerialPortDelegate Optionals
353
extension SerialPortDelegate {
354
}