は!・か!!・た!!!・の塩(Central)

# ゆめみの日常
ある日社内のSlackでこんなやり取りがありました。
<img width="581" alt="スクリーンショット 2019-12-11 12.20.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352032/8f4f8a38-d16b-bdf9-f531-98f5c798a07d.png">


**※伯方の塩は博多でつくっているわけではないそうです。**

このスレッドをながめていたら、昔Twitterで見た「[ずんどこきよしのプログラム](https://qiita.com/shunsugai@github/items/971a15461de29563bf90)」っぽいものが作れるんじゃないかと思い、CoreBluetoothで作ってみました。

そして出来たのがこちら。(※音が流れるので注意！)
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"> <a href="https://twitter.com/u11238123/status/1198624428143214592">https://twitter.com/u11238123/status/1198624428143214592</a></p><a href="https://twitter.com/u11238123/status/1198624428143214592"></a></blockquote>

# アプリ通信の流れについて
このアプリはCentral(動画内でのiPhone)側からPeripheral(動画内でのiPad)側にwriteをして音声を出力しています。

![IMG_0306.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352032/904a2f53-9ab8-54ea-57be-98e2c2d2a790.png)

Bluetoothは固有の名称などがたくさんあり、それを全て説明していると1つ記事がかけてしまうので詳しく知りたい方は[過去に僕がまとめた記事](http://harumi.sakura.ne.jp/wordpress/2018/11/04/ble-foundation/)があるのでそれを参照してください。

また、この記事では**Central**の実装の解説だけに絞り、音の再生部分などは省略します🙇‍♂️

# centralの実装
Central側でデータを書き込む流れは以下のような感じです。
① Bluetoothの使用許可をinfo.plistに書く
② CBCentralManagerのインスタンスを作る
③ デバイスがBluetoothを使用可能か判定
④ スキャンを開始する
⑤ 検出したperipheralとコネクトする
⑥ peripheralのServiceを検出する
⑦ Serviceの中のCharacteristicを検出する
⑧ 検出したCharacteristicに対してwriteする



### ① Bluetoothの使用許可をinfo.plistに書く
ios13からiosデバイスで[Bluetoothを使用する際permissionの許可が必須](http://harumi.sakura.ne.jp/wordpress/2019/08/27/ios13%E3%81%8B%E3%82%89%E3%81%AEbluetooth%E3%83%91%E3%83%BC%E3%83%9F%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3/)になったため記載してあげる必要があります。

``` xml
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>伯方の塩で使うよ</string>
```

ここに記載した文章がアプリ内のアラートに表示されますが、大手などのアプリでもいい加減なものを複数観測しているので、審査の際は書いてあればOKなのかなと思います。

ですが、ユーザに不信感を与えるので何に使うかは詳しく書いておいた方が良いと思います。

### ② CBCentralManagerのインスタンスを作る

CBCentralManagerのインスタンスを作成すると各種delegateメソッドが呼ばれるようになります。

```swift
class ViewController: UIViewController {
    var centralManager: CBCentralManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }
}
extension ViewController: CBCentralManagerDelegate, CBPeripheralDelegate{}
```
### ③ デバイスがBluetoothを使用可能か判定
状態の取得はPeripheralと同じく以下のようなcaseが用意されています。
poweredOn以外の時に無理やり次に進もうとするとクラッシュする可能性があるので注意が必要です。

```swift
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
            
        case .unknown:
            print("よくわかないけどダメ！")
        case .resetting:
            print("システムとの接続がよくわからんからダメ！")
        case .unsupported:
            print("Bluetooth許可されてないじゃんダメ！")
        case .unauthorized:
            print("このデバイスBluetooth使えないからダメ！")
        case .poweredOff:
            print("Bluetooth offになってるからダメ！")
        case .poweredOn:
            print("つかえるよ！！！")
        @unknown default:
            fatalError("へんなのきたからダメ！")
        }
    }
```
### ④ スキャンを開始する
poweredOnの状態（Bluetoothが使用可能）な場合以下のようにスキャンを開始するメソッドを呼び出します。
[伯方の塩（Peripheral）](https://qiita.com/harumidiv/private/bff68fac18ddaa9eb97b)の記事の方で**uuidgen**を使って作成したUUIDを指定しています。

scanForPeripheralsの第一引数にはnilを指定することも可能で、**nilを指定した場合は見つかった全てのPeripheralが検知のメソッドに飛んできます。**

```swift

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {            
            central.scanForPeripherals(withServices: [CBUUID(string: "19688AFB-4E68-4F21-BCBA-421220280930")], options: nil)
        }
    }
```
### ⑤ 検出したperipheralとコネクトする
scanForPeripheralsが呼ばれて指定したServiceを持つPeripheralを見つけると**centralManager(_ central: CBCentralManager,didDiscover peripheral: CBPeripheral,advertisementData: [String : Any],rssi RSSI: NSNumber)**メソッドが呼ばれます。

この時注意しなくてはいけないのがconnectを行うperipheralはメンバに保持しておかないといけないという点です。
これ以降に呼ばれるメソッドにもperipheralは引数として保持していますが、**ローカルに保持していないperipheralに対してwriteなどを行なっても実行されません。**

```swift
    var myPeripheral: CBPeripheral!

    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {
         myPeripheral = peripheral
         myCentralManager.connect(myPeripheral, options: nil)
    }
```

今回は使用していない2つの引数ですが、advertisementDataはその名のとうりperipheral側でadvertiseで持たせたデータが入っています。

RSSIはperipheralの電波強度が入っています。よくRSSIはcentralとperipheral間の距離だと勘違いしている人がいますが、電波強度なのでperipheralの充電が減ったりすると当然値が変化してしまいます。

そのためRSSI==距離ではないので注意

### ⑥ peripheralのServiceを検出する
コネクトが成功したらメンバとして保持しておいたperipheralにdelegateをセットしてServiceの検索を行います。

こちらもスキャンを行なっていた時同様nilを指定するとコネクトしているPeripheralの全てのServiceを検出します。

```swift
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        myPeripheral.delegate = self
        myPeripheral.discoverServices([CBUUID(string: "19688AFB-4E68-4F21-BCBA-421220280930")])
    }
```
### ⑦ Serviceの中のCharacteristicを検出する
Serviceを検出すると以下のメソッドに渡ってくるのでその中でServiceの保持しているCharacteristicをさらに探しに行きます。

discoverCharacteristicsの第一引数にもnilを渡した場合はServiceの保持しているすべてのCharacteristicがそのまま渡されることになります。

```swift
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if error != nil {
            print(error)
        }
        peripheral.services.forEach { servie in
           if service.uuid.uuidString == "19688AFB-4E68-4F21-BCBA-421220280930" {
               myPeripheral.discoverCharacteristics([CBUUID(string: "999889FF-42B0-4FC8-B5BC-0CAB8C323FD2")], for: service)
           }
        }
    }
```
### ⑧ 検出したCharacteristicに対してwriteする

あとはperipheralに向けて値をwriteするだけです。
冒頭の動画では**0x01, 0x02, 0x03**の値をランダムに送ってperipheral側で音声を出力していました。

```swift
    let sendData: [UInt8] = [0x01, 0x02, 0x03]
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if error != nil {
            print(error)
        }
        
        service.characteristics?.forEach{ characteristic in
            if characteristic.uuid.uuidString == "999889FF-42B0-4FC8-B5BC-0CAB8C323FD2"{
                let data = Data(bytes: [sendData.randomElement()!])
                myPeripheral!.writeValue(data, for: targetCharacteristic, type: .withResponse)
            }
        }
    }
```

writeは必ずこのメソッドの中で呼ばなければいけないかというとそうではなく、このメソッドに到達した時点でwriteできることが可能だということが証明されるものだと考えてください。

peripheralを他のクラスに渡してwriteやread,notifyなどを行うことも可能です。


# Peripheralをiosで実装した際のキャッシュ問題
ios以外でperipheralを作った時は問題ないんですが、CoreBluetoothでPeripheralを実装するともともとデバイスの保持しているServiceやCharacteristicしか検出することができません。
これが[デバイスがGATTをキャッシュしていることが問題](http://harumi.sakura.ne.jp/wordpress/2019/11/24/iphone%e3%81%a7peripheral%e3%82%92%e4%bd%9c%e3%81%a3%e3%81%9f%e6%99%82%e3%81%abcentral%e3%81%8b%e3%82%89%e6%a4%9c%e5%87%ba%e3%81%a7%e3%81%8d%e3%81%aa%e3%81%84%e5%95%8f%e9%a1%8c/)で起こる問題です。

以下のメソッドの中で再度Serviceの検出を行うことで追加したServiceを見つけることができます。

``` swift
    func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
        myPeripheral.discoverServices([CBUUID(string: "19688AFB-4E68-4F21-BCBA-421220280930")])
    }

```