スポンサードリンク

アプリ編#12 たよれるCoreBluetoothニキ Part2【SwiftUI】

スマートカーテン

やぎ星人です。どうもこんにちは。

前回の記事ではCoreBluetoothの概念的な部分について学んだことを綴りました。
今回はCoreBluetoothを実際に組み込んでみたいと思います。

CoreBluetooth組み込み手順

CoreBluetoothを使用する上で必要とした実装内容を以下に記載します。
“BleHandler.swift”というファイルを新設し、CoreBluetoothとのI/Fを担うクラス(BleHandlerクラス)を実装しました。

CoreBluetoothフレームワークのインポート

まず、プロジェクトにCoreBluetoothフレームワークを追加しました。

続いて、info.plistに”Privacy – Bluetooth Always Usage Description”を追加しました。
このプライシー設定を追加しないとiOSデバイスにてBluetooth機能を使用できませんでした。
Value値にはBluetoothを使用する理由を記入します。(アプリの審査時に必要とのこと)
英語力に自信がないのですが、、今回は「To send control instruction to the system」としました。

最後に”BleHandler.swift” ファイルにてCoreBluetoothのインポート処理を実装しました。

import CoreBluetooth

セントラルマネージャー(CBCentralManager)インスタンスの生成

新設したBleHandlerクラスのコンストラクタにてCBCentralManagerインスタンスの生成を行いました。

色々と情報収集した結果、CBCentralManagerとはCoreBluetoothにて提供されるBLE通信における親局(セントラル)としての機能を提供するコンポーネントであると学習しました。
周辺機能(ペリフェラル)デバイスの検索(スキャン)や接続、データの読み書き等の機能を提供してくれます。

class BleHandler :NSObject,CBCentralManagerDelegate, CBPeripheralDelegate{
    var centralManager: CBCentralManager?
    var myPeripheral: CBPeripheral?
…
    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }
…

セントラルマネージャー起動確認

CBCentralManagerインスタンスを生成すると、セントラルマネージャーの状態変化が発生するたびにデリゲート関数”centralManagerDidUpdateState”がコールされ、引数情報よりセントラルマネージャーの状態を知ることができます。

デ、デリゲート関数とは..

知らない概念が出てきたので調べてみました。
ここに書くとごちゃごちゃしちゃうので次記事に纏めることにします。

セントラルマネージャーの状態が”poweredOn”へ遷移したことを確認することで、セントラルマネージャーが起動したことを判断します。セントラルマネージャー起動後、デバイスのスキャン処理(後述)を開始します。

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch (central.state) {
        case .unknown,
             .resetting,
             .unsupported,
             .unauthorized,
             .poweredOff:
             
            // 【ToDo】イベント通知処理
            
            break
        case .poweredOn:
            // デバイスをスキャン
            startScanDevice()
            break
        }
    }

デバイスのスキャン開始/停止

CBCentralManagerクラスにて提供される”scanForPeripherals”関数をコールするとデバイスのスキャンを開始します。

また、”stopScan”関数をコールするとデバイスのスキャン処理を停止します。

    //デバイスのスキャンを開始
    func startScanDevice(){
        centralManager!.scanForPeripherals(withServices: nil, options: nil)
    }

    //デバイスのスキャンを停止
    func stopScanDevice(){
        centralManager!.stopScan()
    }

デバイスへの接続

デバイスのスキャンに成功すると、デリゲート関数”centralManager”がコールされます。スキャンにより検出したデバイス情報が引数で渡されるため、それがスマートカーテンデバイスであるか否かをデバイス名情報より判断します。

スマートカーテンデバイスを検出した場合は、 CBCentralManagerクラスにて提供される”connect”関数をコールすることでデバイスへの接続を要求することができます。

また、デリゲート関数”centralManager”コール時にCBPeripheralクラスのインスタンス(検出したデバイスのインスタンス情報)を引数として受け取ります。こちらについてはデバイス接続後のサービス検索時に参照します。(後述)

    let deviceName: String = "Smart_Curtain"
…
    // デバイスを検出したら呼ばれる
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any], rssi RSSI: NSNumber) {        
        if(nil != peripheral.name){
            if(deviceName == peripheral.name!){
                myPeripheral = peripheral
                centralManager!.connect(myPeripheral!, options: nil)
                //デバイススキャンを停止
                stopScanDevice()
            }
        }       
    }

Serviceの検索

デバイスの接続に成功すると、デリゲート関数”centralManager”がコールされます。CBPeripheralクラスより提供される”discoverServices”関数をコールし ServiceUUIDを指定することで、該当のServiceの検索を行います。

デバイスの接続が失敗した際も デリゲート関数”centralManager”がコールされます。
前述したデバイス検出時のタイミングでも同様にデリゲート関数”centralManager”がコールされるのですが、これはオーバーロードという概念であるとギリギリ思い出すことができました。
オーバーロードとは、同一名の関数を関数プロトタイプ(引数定義)の違いにより複数定義・呼び分ける仕組みのことですね。

    let serviceUUID: [CBUUID] = [CBUUID(string: "FFF0")]
…
    // デバイスへの接続が成功すると呼ばれる
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        myPeripheral!.delegate = self
        //指定されたサービスを探す
        myPeripheral!.discoverServices(serviceUUID)
    }
     
    // デバイスへの接続が失敗すると呼ばれる
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
       // 【ToDo】イベント通知処理(接続失敗)
    }
    
    // サービスの検索が成功したら呼ばれる
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if(error == nil){
            // 【ToDo】イベント通知処理(デバイス接続完了)
        }
        else{
            // 【ToDo】イベント通知処理(接続失敗)
        }
    }

データアクセス

Write/Read

①データの読み書きを行う際は、CBPeripheralクラスより提供される”discoverCharacteristics”関数をコールしCharacteristicUUIDを指定することで、読み書き対象となるCharacteristicの検索を行います。

②Characteristicが検出されるとデリゲート関数”peripheral”がコールされます。その後”writeValue”関数および”ReadValue”関数をコールし、データの読み書きを要求します。

③データ読み出し要求後は、読み出し後に再度デリゲート関数”peripheral”がコールされるため、引数のCBCharacteristicインスタンスより読み出しデータを取得します。

    //BLE_WRITEサービス
    func writeService(data: Data){
        let service: CBService = myPeripheral!.services![0]
        let uuid :[CBUUID] = [CBUUID(string:【ToDo】書き込み対象のCharacteristicUUID)]
        processingUUID = 【ToDo】書き込み対象のCharacteristicUUID
        reqData = data
        myPeripheral!.discoverCharacteristics(uuid, for: service)
    }
    
    //BLE_READサービス
    func readService(){
        let service: CBService = myPeripheral!.services![0]
        let uuid :[CBUUID] = [CBUUID(string:【ToDo】読み出し対象のCharacteristicUUID)]
        processingUUID = 【ToDo】読み出し対象のCharacteristicUUID
        myPeripheral!.discoverCharacteristics(uuid, for: service)
    }
        
    // Characteristics を発見したら呼ばれる
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        //ペリフェラルの保持しているキャラクタリスティクスから特定のものを探す
        for i in service.characteristics!{
            
            switch(i.uuid.uuidString){
            case 【ToDo】書き込み対象のCharacteristicUUID:
                
                    //書き込み
                    peripheral.writeValue(reqData! , for: i, type: .withoutResponse)
                    processingUUID = .none
                }
                
            case 【ToDo】読み出し対象のCharacteristicUUID:
                
                    //Notification を受け取るというハンドラ
                    peripheral.setNotifyValue(true, for: i)
                    
                    //読み出し
                    peripheral.readValue(for: i)
                    processingUUID = .none
                }
                
            default:
                break
            }
        }
    }
    
    // データ読み出しが完了したら呼ばれる
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        
        if(characteristic.uuid.uuidString == 【ToDo】読み出し対象のCharacteristicUUID){
             // 【ToDo】Read値取得処理 XXX = characteristic.value
         }
    }

Notify

Notifyを受け取った場合、デリゲート関数”peripheral”がコールされるため、引数のCBCharacteristicインスタンスより通知されたデータを取得します。

    // Notifyを受け取ったら呼ばれる
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?){
        
        if(characteristic.uuid.uuidString == 【ToDo】Notify対象のCharacteristicUUID){
             // 【ToDo】Notify取得処理 XXX = characteristic.value
         }
    }

動作確認結果

ポニ丸にESP32マイコンを借りて簡易動作確認を行いました。
iOSデバイス(iPhone実機)⇔ESP32マイコン間で接続を行い、データアクセス(Write/Read/Notify)ができることを検証したのですが、写真を残し忘れておりました(泣)
Write処理の確認結果は残っていたので、ご紹介させていただきます。

ESP32マイコンとWindowsPCをUSBケーブルで接続し、BLE受信データをUART転送&ターミナルソフトで確認という手順で動作確認を行いました。
下画像が”IOS_TEST\n”というデータをiOSデバイスよりBLE送信(Write)し、ESP32側で認識した受信値をモニタした結果です。
ASCIIコードで分かりにくいですが、ターミナル上で “IOS_TEST\n” が表示されることを確認できました。

以上、CoreBluetoothフレームワークを組み込み、無事にペリフェラルデバイスとのBLE通信を実現することができました。

次回の記事では、本記事で触れたデリゲートという概念みについてご紹介させていただきますので是非ご覧ください。
(というか個人的にアウトプットしたいだけ)

やぎ星人
やぎ星人

それではまた次回の記事でお会いしましょう!

コメント

タイトルとURLをコピーしました