Acessando vários serviços e características BLE usando RxBluetoothKit

Usar o RxBluetoothKit é uma boa maneira de acessar o dispositivo Bluetooth, especialmente acessando vários serviços / características.

Objetivo

(1) leia a Característica A1 do Serviço A
(2) leia a Característica B1 do Serviço B
(3) assine a Característica B2 do Serviço B e receba a notificação nas mesmas vezes que o valor obtido em (2)

Meio Ambiente

Xcode: 10.1
Swift: 4.2.1
RxBluetoothKit: 5.1.4

RxBluetoothKit

https://github.com/Polidea/RxBluetoothKit

Código fonte

1. Defina os serviços

enum BLEService: String, ServiceIdentifier {
case serviceA = "UUID of ServiceA"
case serviceB = "UUID of ServiceB"

var uuid: CBUUID {
return CBUUID(string: self.rawValue)
}
}

2. Defina as características

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. Digitalize e conecte ao dispositivo BLE

Defino tempos limite de 3 segundos para resolver o erro, quando o bluetooth está desligado ou a central não consegue se conectar ao periférico.
Adotei o take (1), a fim de limitar o evento ligado e o dispositivo a se conectar a 1 cada.
* No iOS 11 e 12, o Control Center não pode desligar o bluetooth, mas muda para o estado desconectado.
Nesse estado, nenhum alerta é mostrado, mesmo se CBCentralManagerOptionShowPowerAlertKey for verdadeiro, mas podemos capturar como um evento de tempo limite de inicialização.

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. Leia vários serviços / características

O que fazer aqui é:
(1) ler CharacteristicA1 de ServiceA
(2) ler CharacteristicB1 de ServiceB
Usando Observable.concat, posso ler vários serviços / características.
* Valores de análise omitidos aqui

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. Seja notificado de acordo com o resultado da leitura

O que fazer aqui é:
(3) inscrever a CharacteristicB2 do ServiceB e receber a notificação nas mesmas vezes que o valor obtido em (2)
Eu chamo esse método em onNext em “4. Ler de vários serviços / características”.

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. Descarte o observável para evitar vazamento de memória

Se eu pegar (n), observable será descartado automaticamente após obter o valor n vezes.
Implementei take (n) em “5. Receber notificação de acordo com o resultado da leitura”, mas não em “4. Ler de vários serviços / características”.
Isso porque se eu implementar em “4. Leitura de vários serviços / características”, a conexão bluetooth será descartada antes de “5. Receber notificação de acordo com o resultado da leitura” e irei obter um erro.

Mas, dessa forma, o observável não será descartado e causará um vazamento de memória.

Minha solução para este problema é manter observável como uma variável membro e descartá-lo no próximo acesso.

func readData() {

if let d = disposable {
d
.dispose()
}

disposable
=
connect
()
.flatMap ... // see "4. Read from Multiple Services/Characteristics"
}