BLEで複数デバイスとコネクトする



Bluetoothを利用した実装として、1アプリ（セントラル）に対し１デバイス（ペリフェラル）というのが基本だと思いますが、ここでは１アプリに対して複数デバイスの接続方法をみていきたいと思います。

なお、ここではBLEに対してある程度の知識がある前提で進めていきます。

## 環境
XCode: 9.2
Swift: 4.0
iOS: 11.1

## 完成形
<img src="https://qiita-image-store.s3.amazonaws.com/0/44142/ada6168e-b720-98bb-3d96-6ea830641faf.gif" width=300>

Scanしたデバイスを一覧で表示し、セルをタップすることで`connect`、`disconnect`を切り替えられるサンプルアプリを実装します。

## 実装

まず、iOSアプリでBLEを利用するためには、[CoreBluetoothフレームワーク](https://developer.apple.com/documentation/corebluetooth)を利用します。（詳しくは、Appleの[CoreBluetoothプログラミングガイド](https://developer.apple.com/jp/documentation/CoreBluetoothPG.pdf)参照）

1アプリにつき1デバイスの実装としては、`CBCentralManagerDelegate`と`CBPeripheralManagerDelegate`に一つのクラスが準拠するサンプルが多いと思います。

複数のデバイスを扱う場合でも単純にそのクラスをデバイス分インスタンスを作ればいいように思えます。

しかし[Apple Developer Forums](https://forums.developer.apple.com/thread/20810)での回答を見る限り、CBCentralManagerのインスタンスは１アプリにつき一つであることが望ましいようです。（Appleのドキュメントなどで明記されているところを自分は見つけられませんでした）

なので、

`CBCentralManagerDelegate`に準拠する`DeviceManager`クラス : 1
`CBPeripheralDelegate`に準拠する`Device`クラス : n

となるような設計で実装します。

### Deviceクラス
```swift
import Foundation
import CoreBluetooth

final class Device: NSObject {
    let peripheral: CBPeripheral
    let rssi: NSNumber
    var state = State.disconnected

    init(peripheral: CBPeripheral, rssi: NSNumber) {
        self.peripheral = peripheral
        self.rssi = rssi
        super.init()
        peripheral.delegate = self
    }
}

extension Device {

    enum State: String, CustomStringConvertible {
        case disconnected
        case connected

        var description: String {
            return rawValue
        }
    }
}


// MARK: - CBPeripheralDelegate

extension Device: CBPeripheralDelegate {}
```

`CBPeripheral`をメンバ変数に持ち、一覧に状態を表示するため`State`を持たせています。
また、`CBPeripheralDelegate`に準拠するためには、`NSObjectProtocol`に準拠する必要があるため`NSObject`を継承します。
今回はコネクトまでの実装なので、`CBPeripheralDelegate`のイベントを受け取る処理は実装していません。`read`や`write`をする場合は、この辺の実装する必要があります。

### DeviceManagerクラス

```swift
import Foundation
import CoreBluetooth

final class DeviceManager: NSObject {
    static let deviceUpdated = Notification.Name("deviceUpdated")
    private let centralManager: CBCentralManager
    private(set) var devices = [Device]() {
        didSet {
            NotificationCenter.default
                .post(name: DeviceManager.deviceUpdated, object: nil)
        }
    }

    override init() {
        centralManager = CBCentralManager(delegate: nil, queue: nil)
        super.init()
        centralManager.delegate = self
    }

    func scan() {
        centralManager.scanForPeripherals(withServices: nil, options: nil)
    }

    func stopScan() {
        centralManager.stopScan()
    }

    func connect(peripheral: CBPeripheral) {
        centralManager.connect(peripheral, options: nil)
    }

    func disconnect(peripheral: CBPeripheral) {
        centralManager.cancelPeripheralConnection(peripheral)
    }

    func removeDevices() {
        devices.removeAll()
    }
}


// MARK: - CBCentralManagerDelegate

extension DeviceManager: CBCentralManagerDelegate {

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print("state: \(central.state.rawValue)")
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        let device = Device(peripheral: peripheral, rssi: RSSI)
        if let index = devices.index(where: { $0.peripheral.identifier == device.peripheral.identifier }) {
            devices[index] = device
        } else {
            devices.append(device)
        }
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        devices.first { $0.peripheral == peripheral }
            .map { $0.state = .connected }
        NotificationCenter.default
            .post(name: DeviceManager.deviceUpdated, object: nil)
    }

    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        devices.first { $0.peripheral == peripheral }
            .map { $0.state = .disconnected }
        NotificationCenter.default
            .post(name: DeviceManager.deviceUpdated, object: nil)
    }
}
```

`CBCentralManager`をメンバ変数に持ち、複数のデバイスを`devices`で管理します。
また`TableView`を更新するための通知として`NotificationCentor`を利用しています。


```swift
    centralManager = CBCentralManager(delegate: nil, queue: nil)
```

`queue`に引数を渡すと任意のスレッドで、`CBCentralManagerDelegate`のイベントを受け取ることができます。デフォルトはメインスレッドです。

```swift
    func scan() {
        centralManager.scanForPeripherals(withServices: nil, options: nil)
    }
```

サンプルなので、`withServices`の引数を`nil`にしていますが、引数の指定が推奨されています。ただしその場合はペリフェラル側のアドバタイズメントパケットに`ServiceUUID`が設定されている必要があります。ここは注意です。


```swift
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        let device = Device(peripheral: peripheral, rssi: RSSI)
        if let index = devices.index(where: { $0.peripheral.identifier == device.peripheral.identifier }) {
            devices[index] = device
        } else {
            devices.append(device)
        }
    }
```

`centralManager.scanForPeripherals`を呼び出したあと、ペリフェラルが見つかると`didDiscover`が呼び出されます。ここで`Device`のインスタンスを作成し新規の場合は`devices`に追加、既存の場合は更新しています。

```swift
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        devices.first { $0.peripheral == peripheral }
            .map { $0.state = .connected }

        NotificationCenter.default
            .post(name: DeviceManager.deviceUpdated, object: nil)
    }

    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        devices.first { $0.peripheral == peripheral }
            .map { $0.state = .disconnected }

        NotificationCenter.default
            .post(name: DeviceManager.deviceUpdated, object: nil)
    }
```

`centralManager.connect`を呼び出し、コネクトが完了すると`didConnect`、`centralManager.cancelPeripheralConnection`を呼び出し、ディスコネクトが完了すると`didDisconnectPeripheral`が呼び出されます。ここで、`Device`の`State`を変更し通知を送っています。

## おわりに
１アプリ、複数デバイスの接続は割とニッチな内容ですが、誰かのためになれば嬉しいです。
`TableView`の実装も含めたものは、githubにあげているので興味がある方は是非見てください。

https://github.com/hirotakan/BLEMultiConnectSample