📱

iOSアプリでBLE機能の実装!Core Bluetoothの使い方

に公開

はじめに

iOSアプリエンジニアとして働いている澁谷です。
技術の発展とともに身近になってきたBluetoothですが、
ソニックムーブ内でもBluetooth絡みの案件を見ることも増えてきました。
今回は「Core Bluetooth」を使用して実際してペリフェラル側からデータを取得するまでの実装方法をまとめていきたいと思います。

Core BluetoothとBluetooth機能の基礎

Bluetooth機能を使用するにはセントラルとペリフェラルと呼ばれる2つのデジタル機器が必要になります。セントラルとペリフェラルは親と子の関係にあり、機能を使う側となるのがセントラル機能を提供する側となるのがペリフェラルです。

Core Bluetoothではセントラルとペリフェラルを操作するクラスが提供されているため、Bluetoothの仕組みを知っていれば比較的容易に機能を使用してペアリングやデータの受け渡しを実装することができるようになっています。

今回はペリフェラル側からデータを取得する機能を実装していきたいので使用するのは以下のクラスとプロトコルになります。


  • CBCentralManagerクラス・・・スキャンや接続などの振る舞いを提供
  • CBCentralManagerDelegateプロトコル・・・状態変化や検出結果を取得
  • CBPeripheralDelegateプロトコル・・・接続したペリフェラル側の状態変化や検出結果を取得

◆ 実装する流れ


  1. NSBluetoothAlwaysUsageDescriptionの追加
  2. ペリフェラルのスキャン
  3. ペリフェラルの検出と接続
  4. サービス/キャラクタリスティックのスキャン
  5. データの読み取り

実装するにあたってBluetooth機能を使用するためのオリジナルクラスを作成していきます。このオリジナルクラスが状態変化などを検知できるように各プロトコルの準拠機能の利用のために接続したペリフェラル、キャラクタリスティックを保持させておく設計にします。また前提条件として以下の情報がわかっているものとします。


  • 接続対象のペリフェラルの名前
  • 使用したいサービスのUUID
  • 使用したいキャラクタリスティックのUUID(read)

NSBluetoothAlwaysUsageDescriptionの追加

参考:NSBluetoothAlwaysUsageDescription

iOSアプリからBluetooth機能を使用するにはアプリ内から連絡先や写真などを参照するときと同様にプロパティリスト内にユーザーに対してアクセスを許可するメッセージを用意する必要があります。
Bluetooth機能の場合はキー:「NSBluetoothAlwaysUsageDescription」を追加し利用する理由を簡潔に記述します。

このキーを追加していない状態でCore Bluetoothを使用しようとすると以下のエラーが発生し、アプリ自体がクラッシュしてしまいます。

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key with a string value explaining to the user how the app uses this data.

このアプリは、使用方法の説明なしでプライバシーに関わるデータにアクセスしようとしたため、クラッシュしました。アプリの Info.plist には、アプリがこのデータをどのように使用するかをユーザーに説明する文字列値を含む NSBluetoothAlwaysUsageDescription キーが含まれている必要があります。

ペリフェラルのスキャン

Core BluetoothはXcode内にすでに組み込まれているため、導入作業は必要なくimport分を記述するだけで使用可能です。

import CoreBluetooth

スキャン機能を実装するためには以下の手順で実装します。


  1. CBCentralManagerDelegateに準拠したクラスの準備
  2. 各プロパティの定義
  3. イニシャライザ内などでデリゲートのセット
  4. 必須のデリゲートメソッドの実装

CBCentralManagerDelegateを準拠させたことによってcentralManagerDidUpdateStateの実装が必須になります。このデリゲートメソッドはセントラルの状態が変化した際に呼ばれるようになっており、サポートしていない場合や電源のON/OFFなどを識別できるようになっています。

class BlueCentralService: NSObject, CBCentralManagerDelegate {
    
    private var centralManager: CBCentralManager?
    private var peripheral: CBPeripheral? 
    private var readCharacteristic: CBCharacteristic?
    
    override init () {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }

    // 実装は必須 セントラルの状態変化を検知
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .unknown:
            print("unknown")
        case .resetting:
            print("resetting")
        case .unsupported:
            print("unsupported")
        case .unauthorized:
            print("unauthorized")
        case .poweredOff:
            print("poweredOff")
        case .poweredOn:
            print("poweredOn")
        default:
            print("default")
        }
    }
}

scanForPeripheralsメソッドを呼びだすことでスキャン処理を開始することができますが、このメソッドはセントラルの状態がpoweredOn時のみ実行することができるようになっています。他の状態のまま実行してもアプリがクラッシュすることはないですが、can only accept this command while in the powered on stateというメッセージがコンソールに出力され、スキャンが開始されることはありません。そのため実行前にcentralManager.stateから現在の状況を取得しチェックしておきます。

func startScan() {
    guard let centralManager = centralManager else { return }
    
    if centralManager.state == .poweredOn {
        centralManager.scanForPeripherals(withServices: nil)
    }
}

ペリフェラルの検出と接続

scanForPeripheralsメソッドを実行することでスキャンが開始され周辺のアドバタイズしているペリフェラル機器を探します。ペリフェラルが検出されるとデリゲートメソッドが呼ばれその中で接続したいペリフェラルと名前が一致しているものを探し、接続処理を実行していきます。

// ペリフェラルが検出された際に呼ばれる
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    guard let name = peripheral.name else { return }
    if  name == "ペリフェラル名" {
        self.peripheral = peripheral
        central.connect(peripheral, options: nil)
        centralManager?.stopScan()
    }
}

**接続が成功(失敗)**すると以下のデリゲートメソッドが呼ばれます。成功した際に呼ばれるデリゲートメソッドの中で後ほどサービスを検索していきます。

// 接続が成功した際に呼ばれる
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("接続成功")
}
// 接続が失敗した際に呼ばれる
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
    print(“接続失敗")
}

サービス/キャラクタリスティックのスキャン

サービスとキャラクタリスティックするためにCBPeripheralDelegateを新たに準拠させておきます。これにより検出時にデリゲートメソッドが実行されるようになります。

class BlueCentralService: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {

サービスを検出するために先ほどのペリフェラル検出成功時に呼ばれる中でdiscoverServicesメソッドを実行します。この際にデリゲートのセットと検索したいサービスのUUIDを明示的に指定します。

// 接続が成功した際に呼ばれる
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("接続成功")
    peripheral.delegate = self
    let services: [CBUUID] = [CBUUID(string: "サービスのUUID")]
    peripheral.discoverServices(services)
}

指定したサービスが検出されと以下のデリゲートメソッドが実行され、その中でキャラクタリスティックを検出していきます。サービスと同じような流れでUUIDを指定し、discoverCharacteristicsメソッドを実行します。

// サービスが検出された時に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    if let services = peripheral.services {        
        for service in services {
            let characteristics =  [CBUUID(string: "キャラクタリスティックのUUID")]
            peripheral.discoverCharacteristics(characteristics, for: service)
        }
    }
}

データの読み取り

キャラクタリスティックが検出されると以下のデリゲートメソッドが呼ばれます。キャラクタリスティックはUUIDで識別していきます。readやwriteなどのキャラクタリスティックのプロパティで識別することも可能ですが、その場合はキャラクタリスティックが複数のプロパティを保持している際にややこしくなるので注意してください。

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let characteristics = service.characteristics {
            for characteristic in characteristics {
                //  if characteristic.properties == .read { // プロパティで識別
                if characteristic.uuid == CBUUID(string: "キャラクタリスティックのUUID") {
                    self.readCharacteristic = characteristic
                }
            }
        }
    }

これで目的のキャラクタリスティックが取得できたのでデータを取得してみます。データを取得するにはreadValueメソッドを実行し引数に対象のキャラクタリスティックを渡します。

public func readData() {
    if readCharacteristic != nil {
        connectPeripheral.readValue(for: readCharacteristic)
    }
}

正常に値が取得できると以下のデリケートメソッドが実行され、characteristic.valueデータ本体を参照することが可能です。

// データを取得した際に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    print("キャラクタリスティックの値:\(characteristic.value)")
}

参考

株式会社ソニックムーブ

Discussion