Grow up

生活とプログラミング

iOS Core Bluetooth (Swift) を使用してみた

はじめに

今回はCore Boluetoothを使用してマイコンに文字列を送信してみました。
用語の意味を理解する事に苦労したので概要に意味をまとめています。

Core Bluetooth の概要

クライアント/サーバの考え方が基盤になっています。

ペリフェラル : サービスを提供 (サーバ)
セントラル  : サービスを利用 (クライアント)

f:id:knkomko:20190716002250p:plain:w350


ペリフェラルは見つけてもらう為にアドバタイズパケットと呼ばれる自身の情報を発信しています。
セントラルはアドバタイズパケットを探して周囲のペリフェラルを見つけます。

f:id:knkomko:20190716003642p:plain:w350


接続に成功したペリフェラルと通信する際は、キャラクタリスティックというオブジェクトを使用します。
例えばペリフェラルが天気のサービスを提供するとすれば、気温や湿度というキャラクタリスティックがあります。

f:id:knkomko:20190716010259p:plain:w350

Core Bluetooth のコード

CoreBluetooth をインポートします。使用するクラスには CBCentralManagerDelegate, CBPeripheralDelegate が必要です。
centralManagerDidUpdateState メソッドが無いとビルドエラーになります。

import CoreBluetooth

class bluetoothService: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
   
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch (central.state) {
        case .PoweredOff:
            // BLE PoweredOff
        case .PoweredOn:
            // BLE PoweredOn
        case .Resetting:
            // BLE Resetting
        case .Unauthorized:
            // BLE Unauthorized
        case .Unknown:
            // BLE Unknown
        case .Unsupported:
            // BLE Unsupported
        }
    }
 …
}


周囲のペリフェラルを見つけるためにCBCentralManagerを使用します。

    var centralManager: CBCentralManager?

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


scanForPeripheralsメソッドの引数withSerivicesにnilを渡すと全てのペリフェラルを探してくれます。nilの代わりにサービスのUUIDを渡すとUUIDが一致するペリフェラルを探します。

    func scanStart() {
        if manager!.isScanning == false {
            // サービスのUUIDを指定しない
            centralManager!.scanForPeripherals(withServices: nil, options: nil)
            
            // サービスのUUIDを指定する
            // let service: CBUUID = CBUUID(string: "サービスのUUID")
            // centralManager!.scanForPeripherals(withServices: service, options: nil)
        }
    }


ペリフェラルが見つかると centralManager のデリゲートメソッドが呼び出されます。今回は見つけたペリフェラルは配列に保存して、後で目的のペリフェラルを探して接続するようにしました。

    var peripherals: [CBPeripheral] = []

     func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        peripherals.append(peripheral)
    }


配列から目的のペリフェラルをデバイス名で探して connectメソッドで接続しています。データを送信する際に CBPeripheral が必要になるので目的のペリフェラルはメンバ変数に保持します。

    var cbPeripheral: CBPeripheral? = nil    

    func connect() {
        for peripheral in peripherals {
            if peripheral.name != nil && peripheral.name == "デバイス名" {
                cbPeripheral = peripheral
                manager?.stopScan()
                break;
            }
        }
        
        if cbPeripheral != nil {
            manager!.connect(cbPeripheral!, options: nil)
        }
    }


第二引数が違う centralManager デリゲートメソッドが接続の可否によって呼び出されます。接続が成功した場合には、通信に使うサービスをdiscoverServicesメソッドで探します。

    // 接続が成功すると呼ばれるデリゲートメソッド
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        cbPeripheral?.delegate = self
        
        // 指定のサービスを探す
        let services: [CBUUID] = [CBUUID(string: "サービスのUUID")]
        cbPeripheral!.discoverServices(services)
       
        // すべてのサービスを探す
        // cbPeripheral!.discoverServices(nil)
    }
    
    // 接続が失敗すると呼ばれるデリゲートメソッド
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
       println("connection failed.")
    }


サービスが見つかるとperipheral デリゲートメソッドが呼び出されます。見つかったサービスの中から今度は discoverCharacteristics メソッドを使用してキャラクタリスティックを探します。

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        let serviceUUID: CBUUID = CBUUID(string: "サービスのUUID")
        for service in cbPeripheral!.services! {
            if(service.uuid == serviceUUID) {
                cbPeripheral?.discoverCharacteristics(nil, for: service)
             }
        }
    }


キャラクタリスティックが見つかると引数が違うperipheralデリゲートメソッドが呼び出されます。
今回使用したマイコンにはNotifyとWriteという2種類のキャラクタリスティックがあったためそれぞれ識別しています。
属性がWriteのキャラクタリスティックを使用して文字列データをマイコンに送信するため、そちらはメンバ変数に保持しています。

    var writeCharacteristic: CBCharacteristic? = nil

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {       
        for characreristic in service.characteristics!{
            if characreristic.uuid.uuidString == "属性がNotifyのキャラクタリスティックのUUID" {
                //Notificationを受け取るハンドラ
                peripheral.setNotifyValue(true, for: i)
            }
            
            if characreristic.uuid.uuidString == "属性がWriteのキャラクタリスティックのUUID" {
                writeCharacteristic = characreristic
            }
        }
    }


蛇足ですが今回使用したマイコンが「ペリフェラルの接続成功」と「Notifyが有効になる」場合に接続が確立されるとのことで setNotifyValue メソッドを実行するとマイコンの接続中LEDが緑色に点灯してくれました。
f:id:knkomko:20190716012054j:plain:w150


属性がWriteのキャラクタリスティックを使用して文字列データの送信を行います。
writeValue メソッドを使用することで UTF8 に変更した文字列データをマイコンに送信しています。

    func  sendMessage(message: String) {
        var command = message + "\n"
        let data = command.data(using: String.Encoding.utf8, allowLossyConversion:true)
        cbPeripheral!.writeValue(data! , for: writeCharacteristic!, type: .withoutResponse)
    }


ペリフェラルが文字列データの受信に成功するとNotificationが返ってきます。
そのNotificationを検出すると引数が違う peripheral デリゲートメソッドが呼び出されます。

    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
            println("error \(error)")
            return
        }

        println("received Notification")
    }


Core Bluetooth を使用する中で受信を行う場合には以下のデリゲートメソッドを使用します。

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        let notify: CBUUID = CBUUID(string: "ペリフェラルから値が受信できるキャラクタリスティックのUUID")
        if characteristic.uuid.uuidString == notify.uuidString {
            let message = String(bytes: characteristic.value!, encoding: String.Encoding.ascii)
            println("received \(message)")
        }
    }