【Swift UI】Core BluetoothでPeripheralの実装方法!アドバタイズとは?
この記事からわかること
- Swift UIでCore BluetoothでBluetooth接続アプリの実装方法
- Peripheral側の実装
- ServiceとCharacteristicの実装方法
- Centralに値を渡すには?
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
今回はiOSアプリでBluetooth機能を実装するためにCore Bluetoothを使用してPeripheral側の実装方法をまとめていきます。この記事とCentral側も実装することで実際に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機能の実装方法
流れ
- Privacy - Bluetooth Peripheral Usage Descriptionキーの追加
- Bluetooth機能管理クラスの作成
- サービス/キャラクタリスティックのUUIDを定義
- CBPeripheralManagerDelegateへの準拠
- サービス/キャラクタリスティックを追加する
- アドバタイズの実装の実装
- ペリフェラルの検出と接続
- サービス一覧を取得する
- キャラクタリスティック一覧を取得する
- ペリフェラルから値を取得する
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
続いてCBPeripheralManagerDelegate
にBluePeripheralManager
クラスを準拠させていきます。CBPeripheralManagerDelegate
のデリゲートメソッドからPeripheralとしての状態などを取得することが可能です。準拠させることでperipheralManagerDidUpdateState(_: CBPeripheralManager)
デリゲートメソッドの実装が必須になります。これはPeripheralの状態が変化するタイミングで呼ばれます。
// ① CBPeripheralManagerDelegateへの準拠
extension BluePeripheralManager: CBPeripheralManagerDelegate {
// ①Peripheralの状態が変化するタイミング (実装必須)
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
//
}
}
引数のperipheral
のstate
プロパティから現在の状態を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の引数
- type:サービスのUUID
- primary: true / false
// ②:サービス/キャラクタリスティックを追加する
private func addService() {
// サービスの生成
service = CBMutableService(type: serviceUUID, primary: true)
}
続いてCBMutableCharacteristic
をインスタンス化します。イミュータブルなCBCharacteristic
もあるので間違えないように注意してください。今回は4種類のキャラクタリスティックを準備していきます。
CBMutableCharacteristicの引数
- type:キャラクタリスティックのUUID
- properties:read / write(writeWithoutResponse) / notify / indicate (複数指定したい場合は配列で渡す)
- value:nil 初期値にデータを渡すことも可能だが後から更新できなくなるので注意
- 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
の中に辞書形式で情報を構築していきます。キー値にはCBAdvertisementDataLocalNameKey
やCBAdvertisementDataServiceUUIDsKey
などあらかじめ定義されているものを使用します。
これでアドバタイズの際に名前やサービス情報を一緒に含ませることができます。これをすることでセントラル側からペリフェラルを識別しやすくなります。
/// ③:アドバタイズの開始
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ページくらいあるのでボリュームがすごいですが、ここから得られる知識は数知れませんでした。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。