4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ゆめみ その2Advent Calendar 2019

Day 20

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

Last updated at Posted at 2019-12-19

ゆめみの日常

ある日社内のSlackでこんなやり取りがありました。
スクリーンショット 2019-12-11 12.20.19.png

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

このスレッドをながめていたら、昔Twitterで見た「ずんどこきよしのプログラム」っぽいものが作れるんじゃないかと思い、CoreBluetoothで作ってみました。

そして出来たのがこちら。(※音が流れるので注意!)

アプリ通信の流れについて

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

IMG_0306.PNG

Bluetoothは固有の名称などがたくさんあり、それを全て説明していると1つ記事がかけてしまうので詳しく知りたい方は過去に僕がまとめた記事があるのでそれを参照してください。

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

centralの実装

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

① Bluetoothの使用許可をinfo.plistに書く

ios13からiosデバイスでBluetoothを使用する際permissionの許可が必須になったため記載してあげる必要があります。

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

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

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

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

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

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以外の時に無理やり次に進もうとするとクラッシュする可能性があるので注意が必要です。

    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)の記事の方でuuidgenを使って作成したUUIDを指定しています。

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


    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などを行なっても実行されません。

    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を検出します。

    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がそのまま渡されることになります。

    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側で音声を出力していました。

    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をキャッシュしていることが問題で起こる問題です。

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

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

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?