どうですか?便利ですよね!
アプリはgithubに公開してあります。
https://github.com/toshi-saito/RemoteVolume
(ボリュームの変更はprivate APIなので多分審査通らない。)
スクリーンショット
単純です。UISliderが並んでるだけ!あと名前のラベル。
アプリ実装概要
通信はBLEです。
iPhoneは5以上。iPadはiPad 3rd gen(新しいiPad)から対応しています。
BLEはだいぶ前から触ってるので、サクッといけるだろうと思ってましたが、ペリフェラル(子機)は実装したことなかったので、そこでちょっとつまづきました。
フォアグラウンド時はセントラルとして振る舞い、バッククラウンド時はペリフェラルになります。
ペアリングは省いているので、アプリをインストールして、一度起動するだけで設定は完了です。
近くにデバイスがあれば、勝手につながってボリューム調整できます。
実際動かす時は、UUIDを
https://www.uuidgenerator.net/
とかで作って書き換えて使ってください。
やらないと、お隣さんのiPadいじっちゃった!とか大変あかんことになります。
通信プロトコル
プロトコルって大げさですが、ボリュームがいじれるようになるまではだいたいこんな感じです。
※BLEのスキャン、サービス、キャラクタリスティックの購読などは省略。
- BLE接続が確立したら、CentralがPeripheralに"REQ_NAME"を送りつけます。
- Peripheralは"REQ_NAME"を受信したら、デバイス名をCentralに送りつけます。
- Centralはデバイス名を受け取ったら、すぐさま"REQ_VOLUME"を送りつけます。
- Peripheralは現在のボリューム値(0~1)を送りつけます。
- TableViewCellをもらった情報で書き換えてボリュームをenableにします。
そのあとは、ボリュームの変更をそのままPheripheralに送りつけまくるだけです。
Pheripheralは"REQ_NAME"や"REQ_VOLUME"など指定のコマンド以外の時は、もらった値をDoubleに変換してボリューム値にするようになっています。
BLEは、サービス1つにキャラクタリスティック2つをぶら下げています。
一つは書き込み用(Write)で、もう一つは通知用(Notify)です。
BlendMicroやBLE nanoなどIoTボードのサンプルもだいたいこんな構成だったので、あんまり考えずとりあえずこれでいいやーって感じです。
WriteはCentral->Peripheralの通信に
NotifyはPheripheral->Centralの通信に使用しています。
基本的に、Centralが要求投げてPheripheralがそれに答えるって感じです。
Pheripheral実装
Peripheral.swift
func peripheralManager(peripheral: CBPeripheralManager, didReceiveWriteRequests requests: [CBATTRequest]) {
// データをStringに強制的に変換してみる
let data = requests[0].value
let dataStr: NSString? = NSString(data:data!, encoding:NSUTF8StringEncoding)
print("Receive: %@", dataStr)
if (dataStr == Peripheral.REQUEST_NAME) {
// 名前要求だ!
let d = NSMutableData()
var bytes = [UInt8]()
for char in UIDevice.currentDevice().name.utf8{
bytes += [char]
}
d.appendBytes(bytes, length: bytes.count)
let ok = peripheral.updateValue(d, forCharacteristic: rxCharacteristic!, onSubscribedCentrals: nil)
print("Send REQUEST_NAME: %@", ok)
} else if (dataStr == Peripheral.REQUEST_VOLUME) {
// ボリューム要求だ!
let d = NSMutableData()
let f = toByteArray(Volume.get())
d.appendBytes(f, length: f.count)
let ok = peripheral.updateValue(d, forCharacteristic: rxCharacteristic!, onSubscribedCentrals: nil)
print("Send REQUEST_VOLUME: %@", ok)
} else {
// それ以外!
// Doubleに強制変換!そのままセット。
let v = UnsafePointer<Double>(data!.bytes).memory
Volume.set(v)
}
}
もうほんとそのままです。
データを送りつけるときに、Stringを[UInt8]に変換しないといけないくらいです。
その変換は↓で行けます。(ググったら出てきた。そのまま使ってる)
func toByteArray<T>(var value: T) -> [UInt8] {
return withUnsafePointer(&value) {
Array(UnsafeBufferPointer(start: UnsafePointer<UInt8>($0), count: sizeof(T)))
}
}
ちなみに、BLEのアドバタイズデータにはローカルネームを乗せられるんで、
端末名はそれで渡せばいいじゃねーか!と思うかもしれませんが、
iOSのPeripheralはバックグラウンドに入ると、ローカルネームをアドバタイズしてくれなくなります。仕様なのでしょうがないです。
Central実装
手抜きでViewController.swiftに直書き。
// キャラクタリスティックの購読が正常に終わるとこれが呼ばれます。(本当はerror=nilの確認が必要)
func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) {
for c in service.characteristics! {
if c.UUID.UUIDString == Peripheral.TX_CHARA.UUIDString {
txCaracteristic = c
}
if c.UUID.UUIDString == Peripheral.RX_CHARA.UUIDString {
rxCaracteristic = c
}
}
// 2つとも購読できたようなら、"REQ_NAME"をリクエストする
if txCaracteristic != nil && rxCaracteristic != nil {
self.peripheral?.setNotifyValue(true, forCharacteristic: self.rxCaracteristic!)
requestName()
}
}
// Peripheralから書き込みがあるとこれが呼ばれます。
func peripheral(peripheral: CBPeripheral, didUpdateValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) {
let data = characteristic.value
print("Update: %@", data)
// ペアリングの進行状況はフラグで管理。
if (requestingName) {
requestingName = false
let str = NSString(data:data!, encoding:NSUTF8StringEncoding)
// リクエスト投げてもnilが帰ってくることがあるので(なんで?)、その場合は再度リクエストしちゃう!
if str != nil {
name = str as! String
// 次はボリュームリクエスト。
requestCurrentVolume()
} else {
requestName()
}
return
}
// ペアリング終わった
requestingCurrentVolume = false
value = UnsafePointer<Double>(data!.bytes).memory
print("Update Value: %@", value)
dispatch_async_main {
self.tableView?.reloadData()
}
}
ペアリングの進行状況はフラグで管理しています。単純にリクエストした直後のレスポンスはそのレスポンスだよね。ってだけです。
コードのほとんどはBLEの基本的な処理(サービスの購読とかスキャンとか)なので、通信部分だけだとだいぶコード少ないです。
注意としては、Pheripheralとつなぎっぱなしでバックグラウンドに入ると、BLEのコネクションが強制破棄されてしまいます。
それだと、Peripheralに切断された通知がいかず、
Peripheral側で一定時間通信を復帰しようとします。
が、対象のCentralはもういないので、最終的にデバイスとの切断が切れました。
というダイアログが出ちゃいます。
それを出さなくするため、バックグラウンド移行時にペリフェラルとの接続を明示的に切断しましょう。
func disconnectAll() {
for c in list {
manager?.cancelPeripheralConnection(c.peripheral!)
}
list = []
}
核心のボリューム変更
MPMusicPlayerControllerを使えばいける。
MPMusicPlayerController *playerController = [MPMusicPlayerController systemMusicPlayer];
[playerController setValue:@(volume) forKey:@"volume"];
[playerController setValue:@(volume) forKey:@"volumePrivate"];
まとめ
ということで、BLE楽しいよ!
メリークリスマス!!