Help us understand the problem. What is going on with this article?

iPhoneからiPadのボリュームを変更する

More than 3 years have passed since last update.

つまるところ遠隔操作です。
gifアニをご覧ください。
animation gif

どうですか?便利ですよね!

アプリはgithubに公開してあります。
https://github.com/toshi-saito/RemoteVolume
(ボリュームの変更はprivate APIなので多分審査通らない。)

スクリーンショット

IMG.png
単純です。UISliderが並んでるだけ!あと名前のラベル。

アプリ実装概要

通信はBLEです。
iPhoneは5以上。iPadはiPad 3rd gen(新しいiPad)から対応しています。

BLEはだいぶ前から触ってるので、サクッといけるだろうと思ってましたが、ペリフェラル(子機)は実装したことなかったので、そこでちょっとつまづきました。

フォアグラウンド時はセントラルとして振る舞い、バッククラウンド時はペリフェラルになります。
ペアリングは省いているので、アプリをインストールして、一度起動するだけで設定は完了です。
近くにデバイスがあれば、勝手につながってボリューム調整できます。
実際動かす時は、UUIDを
https://www.uuidgenerator.net/
とかで作って書き換えて使ってください。
やらないと、お隣さんのiPadいじっちゃった!とか大変あかんことになります。

通信プロトコル

プロトコルって大げさですが、ボリュームがいじれるようになるまではだいたいこんな感じです。
※BLEのスキャン、サービス、キャラクタリスティックの購読などは省略。

  1. BLE接続が確立したら、CentralがPeripheralに"REQ_NAME"を送りつけます。
  2. Peripheralは"REQ_NAME"を受信したら、デバイス名をCentralに送りつけます。
  3. Centralはデバイス名を受け取ったら、すぐさま"REQ_VOLUME"を送りつけます。
  4. Peripheralは現在のボリューム値(0~1)を送りつけます。
  5. 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楽しいよ!
メリークリスマス!!

pluswing
東京都東久留米市にて、 ソフトウェア、アプリ、IoT機器の開発を中心に活動しています。 Youtubeにて開発動画配信も行なっています。 ホームページ: https://pluswing.co.jp Youtube: https://t.co/Rj9ea3vZCS
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away