【Swift UI】Core BluetoothでPeripheralの実装方法!アドバタイズとは?

この記事からわかること

  • Swift UICore BluetoothBluetooth接続アプリ実装方法
  • Peripheral側の実装
  • ServiceCharacteristicの実装方法
  • Central渡すには?

index

[open]

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

みんなの誕生日

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

posted withアプリーチ

今回はiOSアプリでBluetooth機能を実装するためにCore Bluetoothを使用してPeripheral側の実装方法をまとめていきます。この記事とCentral側も実装することで実際に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に掲載しています。

「Read」「Write」「WriteWithoutResponse」「Notify」「Indicate」の違い

Bluetoothでデータはキャラクタリスティックを介して操作されますが、キャラクタリスティックには属性が付与されそれぞれ特徴が異なります。先にざっと確認しておきます。

Read

Readは読み取りをするための属性です。

Write

Writeは書き込みをするための属性です。

WriteWithoutResponse

WriteWithoutResponseは書き込みをするための属性です。Writeとは違い応答がありませんがその分高速にデータの書き込みが可能です。

Notify

Notifyは通知をするための属性です。データの変更があった際に通知することができます。

indicate

Indicateは通知を示唆をするための属性です。Notifyと同じくデータの変更があった際に通知が飛びますが、クライアントが通知を受信したことを確認する必要があります。

Peripheral機能の実装方法

流れ

  1. Privacy - Bluetooth Peripheral Usage Descriptionキーの追加
  2. Bluetooth機能管理クラスの作成
  3. サービス/キャラクタリスティックのUUIDを定義
  4. CBPeripheralManagerDelegateへの準拠
  5. サービス/キャラクタリスティックを追加する
  6. アドバタイズの実装の実装
  7. ペリフェラルの検出と接続
  8. サービス一覧を取得する
  9. キャラクタリスティック一覧を取得する
  10. ペリフェラルから値を取得する

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

Privacy - Bluetooth Peripheral Usage Descriptionキーの追加

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

Privacy - Bluetooth Peripheral Usage Description
Privacy - Bluetooth Always Usage Description

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

Bluetooth機能管理クラスの作成

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

import UIKit
import CoreBluetooth

class BluePeripheralManager:  NSObject, ObservableObject {
    // シングルトン
    static let shared = BluePeripheralManager()
    // Centralマネージャー
    private var peripheralManager: CBPeripheralManager!
    // peripheral側のローカル名を定義
    private var peripheralName = "Test Peripheral"
    // ログ出力用
    @Published  var log = ""

    override init() {
        super.init()
    }
}

またペリフェラルの名称をperipheralNameプロパティに保持しておきます。ここで指定した値でCentral側とPeripheral側がお互いを認識できるようにしておきます。また後で使用するのでinitもオーバーライドしておきます。

サービス/キャラクタリスティックのUUIDを定義

さらにペリフェラルに持たせるサービスとキャラクタリスティックのUUIDと保持用のプロパティを定義します。UUIDはCBUUIDクラスのイニシャライザを使用してCBUUID型で定義しておきます。UUID(Universally Unique IDentifier)とはSwift独自のものではなく識別子の標準規格です。128bit(=16byte)の数値として表されます。

class BluePeripheralManager: NSObject {
    // 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

    // サービス用の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 writeWithoutResponseCharacteristicUUID = CBUUID(string:"00000000-2222-2222-1111-111111111111")
    private let notifyCharacteristicUUID = CBUUID(string:"00000000-3333-1111-1111-111111111111")
    private let indicateCharacteristicUUID = CBUUID(string:"00000000-4444-1111-1111-111111111111")
    
    // サービス/キャラクタリスティック保持用の変数
    private var service:CBMutableService!
    private var readCharacteristic: CBMutableCharacteristic!
    // private var writeCharacteristic: CBMutableCharacteristic!
    private var writeWithoutResponseCharacteristic: CBMutableCharacteristic!
    private var notifyCharacteristic: CBMutableCharacteristic!
    private var indicateCharacteristic: CBMutableCharacteristic!

}

これで下準備の完成です。

CBPeripheralManagerDelegateへの準拠

公式リファレンス:CBPeripheralManagerDelegate

続いてCBPeripheralManagerDelegateBluePeripheralManagerクラスを準拠させていきます。CBPeripheralManagerDelegateデリゲートメソッドからPeripheralとしての状態などを取得することが可能です。準拠させることでperipheralManagerDidUpdateState(_: CBPeripheralManager)デリゲートメソッドの実装が必須になります。これはPeripheralの状態が変化するタイミングで呼ばれます。

// ① CBPeripheralManagerDelegateへの準拠
extension BluePeripheralManager: CBPeripheralManagerDelegate {
    // ①Peripheralの状態が変化するタイミング (実装必須)
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
      // 
    }
}

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

公式リファレンス:CBManagerState

 // ①Peripheralの状態が変化するタイミング (実装必須)
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
    switch peripheral.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
        // Bluetooth接続が開始できるようになります
        log.append("poweredOn\n")
    @unknown  default:
        log.append("default\n")
    }
}

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

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

サービス/キャラクタリスティックを追加する

ペリフェラルにサービスとキャラクタリスティックを追加していきます。addServiceメソッドを定義してその中に記述していきます。サービスはCBMutableServiceをインスタンス化します。イミュータブルなCBServiceもあるので間違えないように注意してください。

CBMutableServiceの引数

  1. type:サービスのUUID
  2. primary: true / false
// ②:サービス/キャラクタリスティックを追加する
private func addService() {
    // サービスの生成
    service = CBMutableService(type: serviceUUID, primary: true)

}

続いてCBMutableCharacteristicをインスタンス化します。イミュータブルなCBCharacteristicもあるので間違えないように注意してください。今回は4種類のキャラクタリスティックを準備していきます。

CBMutableCharacteristicの引数

  1. type:キャラクタリスティックのUUID
  2. properties:read / write(writeWithoutResponse) / notify / indicate (複数指定したい場合は配列で渡す)
  3. value:nil 初期値にデータを渡すことも可能だが後から更新できなくなるので注意
  4. permissions:readable / writeable (複数指定したい場合は配列で渡す)
// サービスの生成
service = CBMutableService(type: serviceUUID, primary: true)

// キャラクタリスティックの生成 4種類 read / write(writeWithoutResponse) / notify / indicate
// 初期値にデータを渡すこともできるが後から上書きできなくなってしまう
readCharacteristic = CBMutableCharacteristic(type: readCharacteristicUUID, properties: .read, value: nil, permissions: .readable)
writeCharacteristic = CBMutableCharacteristic(type: writeCharacteristicUUID, properties: [.read,.write], value: nil, permissions: [.writeable,.readable])
  //  writeWithoutResponseCharacteristic = CBMutableCharacteristic(type: writeWithoutResponseCharacteristicUUID, properties: [.read,.writeWithoutResponse], value: nil, permissions: [.writeable,.readable])
notifyCharacteristic = CBMutableCharacteristic(type: notifyCharacteristicUUID, properties: .notify, value: nil, permissions: .readable)
indicateCharacteristic = CBMutableCharacteristic(type: indicateCharacteristicUUID, properties: .indicate, value: nil, permissions: .readable)

最後にservice.characteristicsに配列形式のキャラクタリスティックを追加し、peripheralにサービスを追加します。

// キャラクタリスティックの追加
service.characteristics =  [
    readCharacteristic,
    writeCharacteristic,
    // writeWithoutResponseCharacteristic,
    notifyCharacteristic,
    indicateCharacteristic
]

// サービスの追加
peripheralManager.add(service)

addServiceメソッドが完成したのでperipheralManagerDidUpdateStateの中のpoweredOn状態で呼び出しておきます。poweredOn状態に入る前に追加処理を実行しても正常に追加できない場合があるので注意してください。

func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
    switch peripheral.state {

    // 〜〜〜〜〜〜省略
    case .poweredOn:
        // 電源がON
        // Bluetooth接続が開始できるようになります
        log.append("poweredOn\n")

        // ②:サービス/キャラクタリスティックを追加する
        addService()
    @unknown  default:
        log.append("default\n")
    }
}

CBPeripheralManagerDelegateに準拠したことでperipheralManager(_ :, didAdd service: , error:)がサービス追加時に呼ばれるようになります。

// ②:サービスが追加完了
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
    if error != nil {
        log.append("サービス追加失敗\n")
    }
    log.append("サービスの追加完了\n")
}

アドバタイズの実装

セントラル側と接続するためにペリフェラル側からはアドバタイズを実行します。これは自身が接続可能状態で存在することをアピールする動作になります。実行するにはstartAdvertisingメソッドを使用し、引数にペリフェラル側の情報(名前や保有するサービス)を渡しておきます。そのためにはadvertisementDataの中に辞書形式で情報を構築していきます。キー値にはCBAdvertisementDataLocalNameKeyCBAdvertisementDataServiceUUIDsKeyなどあらかじめ定義されているものを使用します。

これでアドバタイズの際に名前やサービス情報を一緒に含ませることができます。これをすることでセントラル側からペリフェラルを識別しやすくなります。

/// ③:アドバタイズの開始
public func startAdvertising() {
    if peripheralManager.state == .poweredOn {
        log.append("アドバタイズ開始\n")
        let serviceUUIDs = [serviceUUID]
        // アドバタイズ情報にローカルネームとサービス情報を含める
        let advertisementData:[String:Any] = [
            CBAdvertisementDataLocalNameKey: peripheralName,
            CBAdvertisementDataServiceUUIDsKey: serviceUUIDs
        ]
        peripheralManager.startAdvertising(advertisementData)
    }
}

// ③:アドバタイズの停止
public func stopAdvertising() {
    log.append("アドバタイズ停止\n")
    peripheralManager.stopAdvertising()
}

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

またCBPeripheralManagerDelegateに準拠したことでperipheralManagerDidStartAdvertising(_:, error:)はアドバタイズ成功時に呼ばれるようになります。

// ③:アドバタイズの成功
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
    log.append("アドバタイズ成功\n")
}

これでペリフェラル側の基礎的な実装は完了です。startAdvertisingを呼び出せばペリフェラルからアドバタイズが開始され、セントラルのスキャンとあえば2つの端末が接続されます。

ここからは追加でペリフェラル側からセントラルから送られたreadやwriteなどのリクエストを検知し処理を実装する方法をまとめていきます。

Readリクエストの検知

セントラルからreadリクエストが届くとperipheralManager(_ :, didReceiveRead request:)デリゲートメソッドが呼ばれます。この際にセントラル側に返す値をコントロールすることが可能です。返却したいデータを作成しrequest.valueに格納します。

// ④ Readリクエストを受け取った際の処理
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
    log.append("Readリクエストを受け取った\n")
    if let data = "World".data(using: .utf8) {
        request.value = data
    }
    self.peripheralManager.respond(to: request, withResult: CBATTError.success)
}

peripheralManagerからrespondメソッドを呼び出しrequestと成功失敗のフラグをCBATTError型で指定すればOKです。

Writeリクエストの検知

セントラルからwriteリクエストが届くとperipheralManager(_ :, didReceiveWrite requests:)デリゲートメソッドが呼ばれます。この中でセントラル側から送られてきた値をコントロールすることが可能です。

// ⑤ Writeリクエストを受け取った際の処理(withOutは検知しない)
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
    log.append("writeリクエストを受け取った\n")
    for request in requests {
        self.readCharacteristic.value = request.value
        log.append("\(request.value)\n")
    }
    self.peripheralManager.respond(to: requests[0], withResult: CBATTError.success)
}

Notifyを送信する

ペリフェラルからNotifyを送信するためにはupdateValueメソッドを使用します。引数に送信したいデータ、キャラクタリスティックを渡します。これはデリゲートメソッドではないのでButtonと紐付けて明示的に呼び出す必要があります。

// ⑥ Notifyを送信するためのカスタムメソッド
public func sendNotify() {
    if let data = "Notify".data(using: .utf8) {
        log.append("notifyを送信\n")
        self.notifyCharacteristic.value = data
        peripheralManager.updateValue(data, for: notifyCharacteristic, onSubscribedCentrals: nil)
    }
}

おすすめ参考書: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