複数のBLEサービス/キャラクタリスティックにアクセスする必要があり、RxBluetoothKitを使って実現しました。
やりたいこと
以下のことを実行する必要がありました。
(1) ServiceA の CharacteristicA1 をread
(2) ServiceB の CharacteristicB1 をread
(3) ServiceB の CharacteristicB2 をsubscribeし、(2)の回数分だけ飛んでくるnotifyを受け取る
環境
Xcode: 10.1
Swift: 4.2.1
RxBluetoothKit: 5.1.4
RxBluetoothKit
ソースコード
1. Serviceの定義
enum BLEService: String, ServiceIdentifier {
case serviceA = "ServiceAのUUID"
case serviceB = "ServiceBのUUID"
var uuid: CBUUID {
return CBUUID(string: self.rawValue)
}
}
2. Chracteristicの定義
enum BLECharacteristic: String, CharacteristicIdentifier {
case charA1 = "CharacteristicA1のUUID"
case charB1 = "CharacteristicB1のUUID"
case charB2 = "CharacteristicB2のUUID"
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. BLEデバイスのスキャン・接続
電源OFF・接続できないときにエラーとして返すため、タイムアウト3秒を設定しました。
またpoweredOn状態になったイベント、接続先デバイスを各1つに限定するために、take(1)を入れています。
※iOS11, 12では、Control CenterからBluetoothが完全オフにはできなくなり、「未接続」状態へ切り替えるようになっています。
その状態では CBCentralManagerOptionShowPowerAlertKey を設定していてもアラートは出ません。
poweredOn状態にもならないため、そのタイムアウトとして捕捉できます。
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
次にこれらを行います。
(1) ServiceA の CharacteristicA1 をread
(2) ServiceB の CharacteristicB1 をread
Observable.concat で複数の処理をまとめることができます。
※受け取りデータの変換処理は省略
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
// notify受け取りの呼び出し
getNotify(char: char, notifyCount: Int(data))
}, onError: { (error) in
// エラー処理
}, onCompleted: {
}, onDisposed: {
})
}
5. readの結果に基づいたnotify受け取り
これを行います。
(3) ServiceB の CharacteristicB2 をsubscribeし、(2)の回数分だけ飛んでくるnotifyを受け取る
このメソッドの呼び出しは、先ほどの「4. 複数サービスの read」の onNext で行なっています。
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
// エラー処理
}, onCompleted: {
}, onDisposed: {
})
}
6. メモリリーク防止のため、Observableのdispose
take(n)を行えば、n回ぶん値を受け取ったあと、自動的にObservableがdisposeされます。
「5. readの結果に基づいたnotify受け取り」ではそれを実装していますが、「4. 複数サービスのread」では実装していません。
「4. 複数サービスのread」で実装すると、「5. readの結果に基づいたnotify受け取り」を行う前にBluetooth接続がdisposeされてしまい、エラーになってしまうためです。
ただし、そうするとObservableが解放されず、メモリリークが発生してしまいます。
そこで readData で生成したObservableをメンバ変数として保持し、次回アクセス時にdisposeするように対処しました。
これであれば、何度BLEアクセスを行なってもメモリリークは発生しなくなります。
(あまりきれいな対処ではありませんが…。もし良いアイデアがありましたらご意見いただきたいです)
func readData() {
if let d = disposable {
d.dispose()
}
disposable =
connect()
.flatMap ... // 以下は「4. 複数サービスのread」参照
}
まとめ
個人的にRxを使ったのは初めてだったのですが、RxBluetoothKitを使うことにより、Core Bluetoothをそのまま使うよりもシンプルに書けるかと思います。