【Swift UI】Core BluetoothでCentralの実装方法!ServiceとCharacteristic

この記事からわかること

  • Swift UICore BluetoothBluetooth接続アプリ実装方法
  • Central側の実装
  • ServiceCharacteristic取得方法
  • CBCentralManagerDelegateCBPeripheralDelegate使い方
  • peripheralから取得したり書き込むには?
  • peripheralから更新通知受け取る(notify)には?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

今回はiOSアプリでBluetooth機能を実装するためにCore Bluetoothを使用してCentral側の実装方法をまとめていきます。この記事とPeripheral側も実装することで実際にBluetooth機能をテストできるアプリが完成できるのでハンズオンで試してみてください。

Core Bluetoothとは?

公式リファレンス:Core Bluetooth

Core BluetoothはアプリからBLE(Bluetooth Low Energy)を使用するための機能を提供するライブラリです。デフォルトで組み込まれているので導入作業は必要なくすぐに使用することができます。

Bluetooth接続を用いる場合は例えばPCとキーボードなど接続しあう2つの登場人物が必要になります。2つのうちBluetoothの機能を使用する親側を「Central(セントラル)」機能を提供する子側を「Peripheral(ペリフェラル)」と呼びます。例で言うとPCがセントラル、キーボードがペリフェラルです。

そしてペリフェラルが持っている機能のまとまりをサービス、機能1つ1つをキャラクタリスティックと呼びます。

iOSアプリ側もセントラルであることが多いですが、もちろんペリフェラルとして実装することも可能になっています。この記事は前回(↓)の記事でペリフェラル側を実装した後に接続できるようにセントラル側を実装していますので先にペリフェラル側の実装から読んでください。

実装するにあたって

Xcodeで実装するにあたってシミュレーターではBluetooth機能をサポートしていないのでテストする際には実機が必要になります。ペリフェラル側との連携を試したい場合は実機が2台に必要になるので注意してください。

また今回の全体のコードはGitHubに掲載しています。

Central機能の実装方法

セントラル側を実装する際はペリフェラル側の仕様などが決まってから実装することのが多いと思います。今回は前回の記事で実装した機能を使用できるようにしていきます。

流れ

  1. Privacy - Bluetooth Always Usage Descriptionキーの追加
  2. Bluetooth機能管理クラスの作成
  3. CBCentralManagerDelegateへの準拠
  4. スキャンの実装
  5. ペリフェラルの検出と接続
  6. サービス一覧を取得する
  7. キャラクタリスティック一覧を取得する
  8. キャラクタリスティックから値を取得する

Core Bluetoothではスキャンや探索などの処理を実行すると結果がデリゲートメソッドから取得できる仕組みが多用されています。デリゲートについて詳しく知りたい方は以下の記事を参考にしてください。

Privacy - Bluetooth Always Usage Descriptionキーの追加

iOSアプリでBluetooth機能を使用するためには「info.plist」に以下のキーを追加します。

Privacy - Bluetooth Always Usage Description

valueには「アプリ内でBluetooth機能を使用するため」など、Bluetooth機能を使用する理由を記述しておきます。

Bluetooth機能管理クラスの作成

Bluetooth機能を管理するためのBlueCentralManagerクラスを作成します。この際にimport CoreBluetoothも合わせて記述しておきます。Centralとしての機能を使用する提供するCBCentralManager型のcentralManagerプロパティをとシングルトン用のプロパティを定義しておきます。

import UIKit
import CoreBluetooth

class BlueCentralManager: NSObject, ObservableObject {
    // シングルトン
    static let shared = BlueCentralManager()
    // Centralマネージャー
    private var centralManager: CBCentralManager!
    // ログ出力用
    @Published  var log = ""

    // MARK: - ペリフェラル側の実装に合わせて定義する
    // peripheral側のローカル名を定義
    private var peripheralName = "Test Peripheral"
    
    // サービス用のUUID
    private let serviceUUID = CBUUID(string:"00000000-0000-1111-1111-111111111111")
    
    // キャラクタリスティック用のUUID
    private let readCharacteristicUUID = CBUUID(string:"00000000-1111-1111-1111-111111111111")
    private let writeCharacteristicUUID = CBUUID(string:"00000000-2222-1111-1111-111111111111")
    private let notifyCharacteristicUUID = CBUUID(string:"00000000-3333-1111-1111-111111111111")
    private let indicateCharacteristicUUID = CBUUID(string:"00000000-4444-1111-1111-111111111111")
    
    // キャラクタリスティック保持用
    private var readCharacteristic: CBCharacteristic!
    private var writeCharacteristic: CBCharacteristic!
    private var notifyCharacteristic: CBCharacteristic!
    private var indicateCharacteristic: CBCharacteristic!
    // MARK: - ペリフェラル側の実装に合わせて定義する
    
    override init() {
        super.init()
    }
}

また接続したいペリフェラル情報を各プロパティに保持しておきます。後で使用するのでinitもオーバーライドしておきます。

CBCentralManagerDelegateへの準拠

公式リファレンス:CBCentralManagerDelegate

続いてCBCentralManagerDelegateBlueCentralManagerクラスを準拠させていきます。CBCentralManagerDelegateデリゲートメソッドからCentralとしての状態や周辺機器の検出結果などを取得することが可能です。準拠させることでcentralManagerDidUpdateState(_: CBCentralManager)デリゲートメソッドの実装が必須になります。これはCentralの状態が変化するタイミングで呼ばれます。

// ① CBCentralManagerDelegateへの準拠
extension BlueCentralManager: CBCentralManagerDelegate {
  // ①Centralの状態が変化するタイミング (実装必須)
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
    // 
  }
}

引数のcentralstateプロパティから現在の状態をCBManagerState型で取得できます。以下は状態に応じて分岐させているコードです。

公式リファレンス:CBManagerState

// ① Centralの状態が変化するタイミング (実装必須)
func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .unknown:
        // 状態不明
        log.append("unknown\n")
    case .resetting:
        // 一時的にリセットされた状態
        log.append("resetting\n")
    case .unsupported:
        // デバイスがBluetooth機能をサポートしていない
        log.append("unsupported\n")
    case .unauthorized:
        // 使用許可がされていない
        log.append("unauthorized\n")
    case .poweredOff:
        // 電源がOFF
        log.append("poweredOff\n")
    case .poweredOn:
        // 電源がON
        log.append("poweredOn\n")
    @unknown  default:
        log.append("default\n")
    }
}

最後にCBCentralManagerDelegateに準拠したことでcentralManagerプロパティにCBCentralManagerインスタンスを格納できるようになります。引数delegateにはself(自身)queueにはnilを渡しておきます。

override init() {
    super.init()
    // ① インスタンスの格納
    centralManager = CBCentralManager(delegate: self, queue: nil)
}

スキャンの実装

Bluetoothで接続できるペリフェラルをスキャンするにはscanForPeripheralsメソッドを使用します。これは周辺にあるペリフェラルを全てスキャンします。withServicesnilを渡すと無条件でマッチするものを検索しますが、サービスのUUIDを渡すことでスキャンを絞ることが可能です。使用するサービスが確定している場合は指定した方が効率が上がります。

// ②:スキャンの実装
public func startScan() {
    if centralManager.state == .poweredOn {
        log.append("スキャン開始\n")
        centralManager.scanForPeripherals(withServices: nil, options: nil)
    }
}

またscanForPeripheralsメソッドは明示的にstopするまでスキャンし続けてしまい電池を無駄に消耗してしまうので、後述するstopScanメソッドを使用して適切なスキャンの停止の実装が必要です。(例えば10秒のタイマーを設置するなど)

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

検出されたペリフェラルをクラス内で保持しておくために新しくプロパティを定義しておきます。

// ③:ペリフェラルの検出
private var connectPeripheral: CBPeripheral!

// ログ出力用
@Published  var log = ""

scanForPeripheralsを実行してペリフェラルが検出されるとcentralManager(_:,didDiscover peripheral:,advertisementData:,rssi)メソッドが呼ばれます。引数のperipheralから検出されたペリフェラルを参照することができます。ここでは定義しておいたペリフェラルの名前と一致するペリフェラルがあれば接続処理をconnectメソッドを使用して実装しています。

    // ③:ペリフェラルの検出
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    // advertisementDataからローカル名を取得
    if let localName = advertisementData["kCBAdvDataLocalName"] as? String{
        // 取得したローカル名とマッチしたいペリフェラル名を比較
        if  localName == self.peripheralName {
            log.append("対象のペリフェラル:\(localName)を検出\n")
            self.connectPeripheral = peripheral
            // ペリフェラルと接続
            central.connect(peripheral, options: nil)
            // スキャンの停止
            centralManager.stopScan()
        }
    }
}

また接続処理を実行したら明示的にstopScanメソッドでスキャンを停止させておきます。

ペリフェラルの接続結果を取得する

connectメソッドで接続リクエストを送信し、接続が成功したらcentralManager(_:,didConnect peripheral:)が失敗したらcentralManager(_:, didFailToConnect peripheral:,error:)が呼ばれます。

// ④:ペリフェラルの接続結果を取得:成功
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    log.append("接続成功\n")
}
// ④:ペリフェラルの接続結果を取得:失敗
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
    log.append("接続失敗\n")
}

サービス一覧を取得する

ペリフェラルとの接続が完了したらそのペリフェラルが持っているサービス一覧とさらにその中のキャラクタリスティック一覧を取得します。そのためにはCBPeripheralDelegateに準拠させる必要があります。さらにextensionしていきます。

// ⑤:サービス/キャラクタリスティックの取得
extension BlueCentralManager: CBPeripheralDelegate {
    
}

少しコードを戻ってペリフェラルの接続が成功した際にperipheral.delegateにデリゲートのセットとサービスの探索を開始するdiscoverServicesメソッドを実装します。引数に探索対象のUUIDを渡して絞り込んでおきます。

    // ④:ペリフェラルの接続結果を取得:成功
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    log.append("接続成功\n")
    // ⑤:サービス/キャラクタリスティックの取得
    peripheral.delegate = self
    // サービスの探索開始
    let services = [serviceUUID]
    peripheral.discoverServices(services) // nilを渡すことも可能だが電池消費が激しい
}

discoverServicesメソッドが実行されサービスが見つかるとperipheral(_:didDiscoverServices error:)デリゲートメソッドが呼ばれます。servicesプロパティから配列形式でサービス一覧が取得できるのでサービス1つ1つに対してdiscoverCharacteristicsメソッドを実行してキャラクタリスティックの探索開始します。ここでも明示的にUUIDを指定することで電池消費を抑えることが可能です。

 // ⑤:サービスが見つかった際に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    log.append("サービスが見つかりました\n")
    // servicesプロパティから配列形式のCBServiceが取得できる
    if let services:Array<CBService> = peripheral.services {
        
        for service in services {
            let characteristicUUIDs = [
                readCharacteristicUUID,
                writeCharacteristicUUID,
                notifyCharacteristicUUID,
                indicateCharacteristicUUID
            ]
            // キャラクタリスティックを探索開始
            // nilを渡すことも可能だが電池消費が激しい
            peripheral.discoverCharacteristics(characteristicUUIDs, for: service)
        }
    }
}

キャラクタリスティック一覧を取得する

discoverCharacteristicsメソッドが実行されキャラクタリスティックが見つかるとperipheral(_:, didDiscoverCharacteristicsFor service:, error:)デリゲートメソッドが呼ばれます。characteristicsプロパティから配列形式でキャラクタリスティック一覧が取得できます。

// ⑤:キャラクタリスティックが見つかった際に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    // characteristicsプロパティから配列形式のCBCharacteristicが取得できる
    if let characteristics:Array<CBCharacteristic> = service.characteristics {
        log.append("キャラクタリスティックは\(characteristics.count)個見つかりました\n")
        for characteristic in characteristics {
            // 一致するUUIDを検索してそれぞれに格納
            if characteristic.uuid == readCharacteristicUUID {
                readCharacteristic = characteristic
            } else if characteristic.uuid == writeCharacteristic {
                writeCharacteristic = characteristic
            } else if characteristic.uuid == notifyCharacteristicUUID {
                notifyCharacteristic = characteristic
            } else if characteristic.uuid == indicateCharacteristicUUID {
                indicateCharacteristic = characteristic
            }
        }
    }
}

キャラクタリスティックの一覧から1つずつUUIDを比較しマッチすればそれぞれのプロパティへ格納しておきます。

キャラクタリスティックから値を取得する

キャラクタリスティックから値を取得するにはreadDataメソッドを新しく定義し、その中で保持しているペリフェラルからreadValueメソッドを呼び出します。引数には読み出し対象のキャラクタリスティックを渡します。

// ⑥ キャラクタリスティックから値を取得する
public func readData() {
    if readCharacteristic != nil {
        connectPeripheral.readValue(for: readCharacteristic)
    }
}

値が取得できるとperipheral(_:, didUpdateValueFor characteristic:, error:)デリゲートメソッドが呼ばれcharacteristicからUUIDや値(Data型)など参照することができます。

// ⑥ キャラクタリスティックから値を取得する
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    log.append("ペリフェラルから値を取得しました。\n")
    log.append("キャラクタリスティックの値:\(characteristic.value)\n")
}

取得した値はData型なので適切に変換して使用する必要があります。

ペリフェラルに値を書き込む

キャラクタリスティックに値を書き込むにregisterDataメソッドを新しく定義し、その中で保持しているペリフェラルからwriteValueメソッドを呼び出します。引数には対象のデータ、書き込み対象のキャラクタリスティックを渡します。

// ⑦ ペリフェラルに値を書き込む
public func registerData() {
    if writeCharacteristic != nil {
        connectPeripheral.writeValue("Hello".data(using: .utf8)!, for: writeCharacteristic!, type: CBCharacteristicWriteType.withResponse)
    }
}

書き込みが成功するとperipheral(_:, didWriteValueFor characteristic:, error:)が呼ばれます。

// ⑦ 書き込み成功時に呼ばれる
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
    log.append("書き込み成功\n")
}

Notifyの通知を受け取る

Notifyはセントラルではなくペリフェラルから値の発火のタイミングが操作されています。セントラル側ではNotifyを検知する状態にしておくことでペリフェラル側からの値の更新を通知します。setNotifyValueメソッドにtrueを渡せば検知状態、falseなら検知を停止させることができます。

// ⑧ Notifyを検知開始する
public func observeNotify() {
    if notifyCharacteristic != nil {
        // Notifyの検知を開始
        connectPeripheral.setNotifyValue(true, for: notifyCharacteristic)
    }
}
// ⑧ Notifyを検知停止する
public func stopNotify() {
    if notifyCharacteristic != nil {
        // Notifyの検知を停止
        connectPeripheral.setNotifyValue(false, for: notifyCharacteristic)
    }
}

Notifyで受け取った値はreadと同じくperipheral(_:, didUpdateValueFor characteristic:, error:)デリゲートメソッドが呼ばれるのでここから取得できます。

// ⑥ キャラクタリスティックから値を取得する
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
  log.append("ペリフェラルから値を取得しました。\n")
  log.append("キャラクタリスティックの値:\(characteristic.value)\n")
}

おすすめ参考書:iOS×BLE Core Bluetoothプログラミング

iOSアプリでBLEを使用した機能を実装したいなら一度は読んでおくことをおすすめする参考書です。iOSでのCore Bluetoothを使用した実装だけでなく、Bluetoothに関する細かい知識やノウハウも詰まっているので網羅的に理解したい方にはバッチリだと思います。

少し古い参考書であり、Objective-CとSwift両方のコードで実装方法が記述されています。Swift UIでの実装方法は載っていませんが、基本的なコードは昔からあまり変わっていないのでつまづくところはなく実装できると思います。

BLEを食材や店員などに例えて解説してくれるので素人でもBLEの概念がつかみやすく記述されています。約500ページくらいあるのでボリュームがすごいですが、ここから得られる知識は数知れませんでした。

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index