Accessing multiple BLE services and characteristics using RxBluetoothKit
Using RxBluetoothKit is a good way to accessing Bluetooth device, especially accessing multiple services/characteristics.
Goal
(1) read CharacteristicA1 of ServiceA
(2) read CharacteristicB1 of ServiceB
(3) subscribe CharacteristicB2 of ServiceB, and get notify as same times as the value got in (2)
Environment
Xcode: 10.1
Swift: 4.2.1
RxBluetoothKit: 5.1.4
RxBluetoothKit
https://github.com/Polidea/RxBluetoothKit
SourceCode
1. Define Services
enum BLEService: String, ServiceIdentifier {
case serviceA = "UUID of ServiceA"
case serviceB = "UUID of ServiceB"
var uuid: CBUUID {
return CBUUID(string: self.rawValue)
}
}
2. Define Chracteristics
enum BLECharacteristic: String, CharacteristicIdentifier {
case charA1 = "UUID of CharacteristicA1"
case charB1 = "UUID of CharacteristicB1"
case charB2 = "UUID of CharacteristicB2"
var uuid: CBUUID {
return CBUUID(string: self.rawValue)
}
var service: ServiceIdentifier {
switch self {
case .charA1:
return BLEService.serviceA
case .charB1, .charB2:
return BLEService.serviceB
}
}
}
3. Scan and Connect to BLE Device
I set 3 seconds timeouts in order to handle error, when the bluetooth is off or the central cannot connect to the peripheral.
I adopted take(1), in order to limit powered on event and device to connect to 1 each.
*In iOS 11 and 12, Control Center cannot kill the bluetooth, but switches to disconnected state.
In that state, no alert is shown even if CBCentralManagerOptionShowPowerAlertKey is true, but we can catch as a timeout of powered on event.
func connect() -> Observable<Peripheral> {
let options = [CBCentralManagerOptionShowPowerAlertKey: true] as [String : AnyObject]
let manager = CentralManager(queue: .main, options: options)
return manager.observeState()
.startWith(self.manager.state)
.filter { $0 == .poweredOn }
.timeout(3.0, scheduler: MainScheduler.instance)
.take(1)
.flatMap { _ in self.manager.scanForPeripherals(withServices: [BLEService.serviceA.uuid, BLEService.serviceB.uuid]) }
.timeout(3.0, scheduler: MainScheduler.instance)
.take(1)
.flatMap { $0.peripheral.establishConnection() }
}
4. Read from Multiple Services/Characteristics
What to do here are:
(1) read CharacteristicA1 of ServiceA
(2) read CharacteristicB1 of ServiceB
By using Observable.concat, I can read from multiple services/characteristics.
*Omitted parsing values here
func readData() {
connect()
.flatMap {
Observable.concat(
$0.readValue(for: BLECharacteristic.charA1).asObservable(),
$0.readValue(for: BLECharacteristic.charB1).asObservable()
)}
.subscribe(onNext: { (char) in
switch char.uuid {
case BLECharacteristic.charA1.uuid:
let data = char.value
case BLECharacteristic.charB1.uuid:
let data = char.value
// call get notify method
getNotify(char: char, notifyCount: Int(data))
}, onError: { (error) in
// error handling
}, onCompleted: {
}, onDisposed: {
})
}
5. Get Notify According to the Result of Read
What to do here is:
(3) subscribe CharacteristicB2 of ServiceB, and get notify as same times as the value got in (2)
I call this method in onNext in "4. Read from Multiple Services/Characteristics".
func getNotify(char : Characteristic, notifyCount : Int) {
char.service.discoverCharacteristics([BLECharacteristic.charB2.uuid]).asObservable()
.flatMap { Observable.from($0) }
.flatMap { $0.observeValueUpdateAndSetNotification() }
.take(notifyCount)
.subscribe(onNext: { (char) in
let data = char.value
}, onError: { (error) in
// error handling
}, onCompleted: {
}, onDisposed: {
})
}
6. Dispose Observable to Prevent Memory Leak
If I take(n), observable will be disposed automatically after getting value n times.
I implemented take(n) in "5. Get Notify According to the Result of Read", but not in "4. Read from Multiple Services/Characteristics".
That's because if I implement in "4. Read from Multiple Services/Characteristics", the bluetooth connection will be disposed before "5. Get Notify According to the Result of Read", and I'll get an error.
But in that way, the observable will not be disposed and it will cause a memory leak.
My solution toward this problem is keeping observable as a member variable and to dispose it in the next access.
func readData() {
if let d = disposable {
d.dispose()
}
disposable =
connect()
.flatMap ... // see "4. Read from Multiple Services/Characteristics"
}
Written by nozomuplus
Related protips
1 Response
I found later that I should dispose also after getting values and in error handling.