ゆめみの日常
※伯方の塩は博多でつくっているわけではないそうです。
このスレッドをながめていたら、昔Twitterで見た「ずんどこきよしのプログラム」っぽいものが作れるんじゃないかと思い、CoreBluetoothで作ってみました。
そして出来たのがこちら。(※音が流れるので注意!)
https://twitter.com/u11238123/status/1198624428143214592
アプリ通信の流れについて
このアプリはCentral(動画内でのiPhone)側からPeripheral(動画内でのiPad)側にwriteをして音声を出力しています。
Bluetoothは固有の名称などがたくさんあり、それを全て説明していると1つ記事がかけてしまうので詳しく知りたい方は過去に僕がまとめた記事があるのでそれを参照してください。
また、この記事ではPeripheralの実装の解説だけに絞り、音の再生部分などは省略します🙇♂️
peripheralの実装
Peripheral側でデータを受け取る流れは以下のような感じです。
① Bluetoothの使用許可をinfo.plistに書く
② CBPeripheralManagerのインスタンスを作る
③ デバイスがBluetoothを使用可能か判定
④ ServiceとCharacteristicをつめる
⑤ アドバタイズ(情報の公開)開始!
⑥ データを受け取る
厳密にアプリを作る際はもっと細かく用意されているメソッドを呼び出してハンドリングなどが必要になりますが、複雑になるので今回は省略します。
①Bluetoothの使用許可をinfo.plistに書く
ios13からiosデバイスでBluetoothを使用する際permissionの許可が必須になったため記載してあげる必要があります。
<key>NSBluetoothAlwaysUsageDescription</key>
<string>伯方の塩で使うよ</string>
ここに記載した文章がアプリ内のアラートに表示されますが、大手などのアプリでもいい加減なものを複数観測しているので、審査の際は書いてあればOKなのかなと思います。
ですが、ユーザに不信感を与えるので何に使うかは詳しく書いておいた方が良いと思います。
② CBPeripheralManagerのインスタンスを作る
コードはこんな感じです。
CBPeripheralManagerのインスタンスが作成されるとデリゲートメソッドが呼ばれはじめます。
class ViewController: UIViewController {
var manager: CBPeripheralManager!
override func viewDidLoad() {
super.viewDidLoad()
self.manager = CBPeripheralManager(delegate: self, queue: nil)
}
}
extension ViewController: CBPeripheralManagerDelegate {}
③ デバイスがBluetoothを使用可能か判定し、可能なら情報をつめる
CBPeripheralManagerのインスタンスを作るとまず最初にperipheralManagerDidUpdateStateというメソッドが呼ばれます。
まず最初にデバイスがアドバタイズ可能かを判定する必要があります。peripheral(デバイス)の状態は引数に持っているperipheralのstateから判別することができます。
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .unknown:
print("よくわかないけどダメ!")
case .resetting:
print("システムとの接続がよくわからんからダメ!")
case .unsupported:
print("Bluetooth許可されてないじゃんダメ!")
case .unauthorized:
print("このデバイスBluetooth使えないからダメ!")
case .poweredOff:
print("Bluetooth offになってるからダメ!")
case .poweredOn:
print("つかえるよ!!!")
@unknown default:
fatalError("へんなのきたからダメ!")
}
}
④ ServiceとCharacteristicをつめる
デバイスが通信可能であれば、アドバタイズ情報の中にServiceとCharacteristicをつめるわけですが、それぞれのUUIDを決める必要があります。
UUID作成用のtarminalコマンド(uuidgen)が用意されているのでそれで作成するのが簡単です。
試しに2つ作成してみました、1つ目をServiceのUUID、2つ目をCharacteristicのUUIDとします。
19688AFB-4E68-4F21-BCBA-421220280930
999889FF-42B0-4FC8-B5BC-0CAB8C323FD2
プロパティとパーミッションは全部盛りにしていますが、今回は実質writeしか使わないのでプロパティはwrite、パーミッションはwritableだけでも問題ありません。
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .poweredOn:
let serviceUUID = CBUUID(string: "19688AFB-4E68-4F21-BCBA-421220280930")
let characteristicUUID = CBUUID(string: "999889FF-42B0-4FC8-B5BC-0CAB8C323FD2")
let service = CBMutableService(type: serviceUUID, primary: true)
let property: CBCharacteristicProperties = [.notify, .read, .write]
let permission: CBAttributePermissions = [.readable, .writeable]
var characteristic = CBMutableCharacteristic(type: characteristicUUID, properties: property, value: nil, permissions: permission)
service.characteristics = [characteristic]
self.manager.add(service)
@unknown default:
//コードが長くなってしまうのでダメなcaseは省略
break
}
}
図解ServiceとCharacteristic
サービスとキャラクタリスティックの図を僕の過去の記事から引っ張ってきました。
今回はサービス1つ、キャラクタリスティック1つの構成ですが、市販されているPeripheralの多くは複数のサービスとキャラクタリスティックを保持しており、図のよう担っていると思います。
⑤ アドバタイズ(情報の公開)開始
サービスの追加が無事成功すると**peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?)**というメソッドが呼ばるので、その中でアドバタイズを行います。
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
if error != nil {
print("失敗してるよ")
}
if self.manager.isAdvertising == false {
let advertiseData = [CBAdvertisementDataLocalNameKey: "博多の塩"]
manager.startAdvertising(advertiseData)
}
}
引数なしでも良いのですが、せっかくなので名前をつけてアドバタイズさせてみました。
iosデバイス同士でやる時の注意なのですが、一度ペアリングを行ってしまうと以降ここで登録した名前ではなくデバイス名が表示されてしまうので注意が必要です!
アドバタイズが問題なくできてるかの確認はLightBlue® Explorerというアプリを使うのが便利です。
アプリを起動すると画像のように近くでアドバタイズを行なっているデバイスを見つけることができます。
⑥ データを受け取る
central側からwriteされた場合は**peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest])**の中にwriteされた20byte以下のData型が飛んでくるのでその中身を見るだけです!
データ型の中身を確認する際は文字列にするのがわかりやすくて良いので以下のようなextendionをData型に生やしておくと便利です。
extension Data {
struct HexEncodingOptions: OptionSet {
let rawValue: Int
static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
}
func hexEncodedString(options: HexEncodingOptions = []) -> String {
let hexDigits = Array((options.contains(.upperCase) ? "0123456789ABCDEF" : "0123456789abcdef").utf16)
var chars: [unichar] = []
chars.reserveCapacity(2 * count)
for byte in self {
chars.append(hexDigits[Int(byte / 16)])
chars.append(hexDigits[Int(byte % 16)])
}
return String(utf16CodeUnits: chars, count: chars.count)
}
}
あとはextensionで生やしたメソッドを使い、値を文字列で検出して実現したい処理をおこなうだけです。
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
let responseData = requests[0].value?.hexEncodedString()
}