USB-Meter / USB Meter / Model / BluetoothSerial.swift
Newer Older
304 lines | 10.9kb
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
    }
69

            
70
    func connect() {
71
        administrativeState = .up
72
        if operationalState < .peripheralConnected {
73
            operationalState = .peripheralConnectionPending
74
            track("Connect caled")
75
            manager.connect(peripheral, options: nil)
76
        } else {
77
            track("Peripheral allready connected: \(operationalState)")
78
        }
79
    }
80

            
81
    func disconnect() {
82
        if operationalState >= .peripheralConnected {
83
            manager.cancelPeripheralConnection(peripheral)
84
            buffer.removeAll()
85
        }
86
    }
87

            
88
    /**
89
     Send data
90

            
91
     - parameter data: Data to be sent.
92
     - 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
93
     */
94
    func write(_ data: Data, expectedResponseLength: Int = 0) {
95
        //track("\(self.expectedResponseLength)")
96
        //track(data.hexEncodedStringValue)
97
        guard operationalState == .peripheralReady else {
98
            track("Guard: \(operationalState)")
99
            return
100
        }
101
        guard self.expectedResponseLength == 0 else {
102
            track("Guard: \(self.expectedResponseLength)")
103
            return
104
        }
105

            
106
        self.expectedResponseLength = expectedResponseLength
107

            
108
//        track("Sending...")
Bogdan Timofte authored 2 weeks ago
109
        guard let writeCharacteristic else {
110
            track("Missing write characteristic for \(radio)")
111
            self.expectedResponseLength = 0
112
            return
Bogdan Timofte authored 2 weeks ago
113
        }
Bogdan Timofte authored 2 weeks ago
114

            
115
        let writeType: CBCharacteristicWriteType = writeCharacteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
116
        peripheral.writeValue(data, for: writeCharacteristic, type: writeType)
Bogdan Timofte authored 2 weeks ago
117
//        track("Sent!")
118
        if self.expectedResponseLength != 0 {
119
            setWDT()
120
        }
121
    }
122

            
123
    func connectionEstablished () {
124
        track("")
125
        operationalState = .peripheralConnected
126
        peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio])
127
    }
128

            
129
    func connectionClosed () {
130
        track("")
131
        operationalState = .peripheralNotConnected
132
        expectedResponseLength = 0
133
        writeCharacteristic = nil
Bogdan Timofte authored 2 weeks ago
134
        notifyCharacteristic = nil
Bogdan Timofte authored 2 weeks ago
135
    }
136

            
137
    func setWDT() {
138
        wdTimer?.invalidate()
139
        wdTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {_ in
140
            track("Response timeout. Expected: \(self.expectedResponseLength) - buffer: \(self.buffer.count)")
141
            self.expectedResponseLength = 0
142
            self.disconnect()
143
        })
144
    }
145

            
Bogdan Timofte authored 2 weeks ago
146
    private func refreshOperationalStateIfReady() {
147
        guard notifyCharacteristic != nil, writeCharacteristic != nil else {
148
            return
149
        }
150
        operationalState = .peripheralReady
151
    }
152

            
153
    private func updateBT18Characteristics(for service: CBService) {
154
        for characteristic in service.characteristics ?? [] {
155
            switch characteristic.uuid {
156
            case CBUUID(string: "FFE1"):
157
                if characteristic.properties.contains(.notify) || characteristic.properties.contains(.indicate) {
158
                    peripheral.setNotifyValue(true, for: characteristic)
159
                    notifyCharacteristic = characteristic
160
                }
161
                if writeCharacteristic == nil &&
162
                    (characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse)) {
163
                    writeCharacteristic = characteristic
164
                }
165
            case CBUUID(string: "FFE2"):
166
                if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
167
                    // DX-BT18 documents FFE2 as the preferred write-only endpoint when present.
168
                    writeCharacteristic = characteristic
169
                }
170
            default:
171
                track ("Unexpected characteristic discovered: '\(characteristic)'")
172
            }
173
        }
174
        refreshOperationalStateIfReady()
175
    }
176

            
177
    private func updatePW0316Characteristics(for service: CBService) {
178
        for characteristic in service.characteristics ?? [] {
179
            switch characteristic.uuid {
180
            case CBUUID(string: "FFE9"): // TX from BLE side into UART
181
                writeCharacteristic = characteristic
182
            case CBUUID(string: "FFE4"): // RX notifications from UART side into BLE
183
                peripheral.setNotifyValue(true, for: characteristic)
184
                notifyCharacteristic = characteristic
185
            default:
186
                track ("Unexpected characteristic discovered: '\(characteristic)'")
187
            }
188
        }
189
        refreshOperationalStateIfReady()
190
    }
191

            
Bogdan Timofte authored 2 weeks ago
192
}
193

            
194
//  MARK:   CBPeripheralDelegate
195
extension BluetoothSerial : CBPeripheralDelegate {
196

            
197
    //  MARK:   didReadRSSI
198
    func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
199
        if error != nil {
200
            track( "Error: \(error!)" )
201
        }
202
        self.RSSI = RSSI.intValue
203
    }
204

            
205
    //  MARK:   didDiscoverServices
206
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
207
        track("\(String(describing: peripheral.services))")
208
        if error != nil {
209
            track( "Error: \(error!)" )
210
        }
211
        switch radio {
212
        case .BT18:
213
            for service in peripheral.services! {
214
                switch service.uuid {
215
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
216
                    peripheral.discoverCharacteristics(Array(Set((BluetoothRadioNotifyUUIDs[radio] ?? []) + (BluetoothRadioWriteUUIDs[radio] ?? []))), for: service)
Bogdan Timofte authored 2 weeks ago
217
                default:
218
                    track ("Unexpected service discovered: '\(service)'")
219
                }
220
            }
221
        case .PW0316:
222
            for service in peripheral.services! {
223
                switch service.uuid {
224
                case CBUUID(string: "FFE0"):
Bogdan Timofte authored 2 weeks ago
225
                    peripheral.discoverCharacteristics(BluetoothRadioNotifyUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
226
                case CBUUID(string: "FFE5"):
Bogdan Timofte authored 2 weeks ago
227
                    peripheral.discoverCharacteristics(BluetoothRadioWriteUUIDs[radio], for: service)
Bogdan Timofte authored 2 weeks ago
228
                default:
229
                    track ("Unexpected service discovered: '\(service)'")
230
                }
231
            }
232
        default:
233
            track("Radio \(radio) Not Implemented!")
234
        }
235
    }
236

            
237
    //  MARK:   didDiscoverCharacteristicsFor
238
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
239
        if error != nil {
240
            track( "Error: \(error!)" )
241
        }
242
        track("\(String(describing: service.characteristics))")
243
        switch radio {
244
        case .BT18:
Bogdan Timofte authored 2 weeks ago
245
            updateBT18Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
246
        case .PW0316:
Bogdan Timofte authored 2 weeks ago
247
            updatePW0316Characteristics(for: service)
Bogdan Timofte authored 2 weeks ago
248
        default:
249
            track("Radio \(radio) Not Implemented!")
250
        }
251
    }
252

            
253
    //  MARK:   didUpdateValueFor
254
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
255
//        track("")
256
        if error != nil {
257
            track( "Error: \(error!)" )
258
        }
259
        buffer.append( characteristic.value ?? Data() )
260
//        track("\n\(buffer.hexEncodedStringValue)")
261
        switch buffer.count {
262
        case let x where x < expectedResponseLength:
263
            setWDT()
264
            //track("buffering")
265
            break;
266
        case let x where x == expectedResponseLength:
267
            //track("buffer ready")
268
            wdTimer?.invalidate()
269
            expectedResponseLength = 0
270
            delegate?.didReceiveData(buffer)
271
            buffer.removeAll()
272
        case let x where x > expectedResponseLength:
273
            // MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema
274
            wdTimer?.invalidate()
275
            expectedResponseLength = 0
276
            buffer.removeAll()
277
            track("Buffer Overflow")
278
        default:
279
            track("This is not possible!")
280
        }
281
    }
282

            
283
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
284
        if error != nil { track( "Error: \(error!)" ) }
285
    }
286

            
287
    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
288
        //track("")
289
    }
290

            
291
}
292

            
293
// MARK: SerialPortDelegate
294
protocol SerialPortDelegate: AnyObject {
295
    // MARK: State Changed
296
    func opertionalStateChanged( to newOperationalState: BluetoothSerial.OperationalState )
297
    // MARK: Data was received
298
    func didReceiveData(_ data: Data)
299
}
300

            
301
// MARK: SerialPortDelegate Optionals
302
extension SerialPortDelegate {
303
}
304