その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

Eddystone と iOS - その2: 実装編

その1 では iOS アプリ開発者から見た、Eddystone を採用するメリット・デメリットについて書きました。本記事は実装編として、Core Bluetooth を用いた Eddystone 検出機能の実装方法 をポイントをかいつまんで紹介したいと思います。




なお、本記事では Core Bluetooth や Bluetooth Low Energy の基礎的なことは省略します。よろしければ下記書籍を参考にしてください。


iOS×BLE Core Bluetoothプログラミング
堤 修一 松村 礼央
ソシム
売り上げランキング: 898

電子書籍版 もあります)

Eddystone を発見する

普通に CBCentralManager でスキャンを開始するだけです。

let services = [CBUUID(string: "FEAA")]
centralManager.scanForPeripheralsWithServices(services, options: nil)

上のコードでは Eddystone ビーコンだけを発見対象とするよう、Eddystone Service UUID `FEAA` を指定しています。

アドバタイズメントデータから Eddystone フレームを取り出す

スキャンしてペリフェラル(ここではEddystoneビーコン)を発見すると、didDiscover〜が呼ばれるわけですが、この引数に入ってくるアドバタイズメントデータから Eddystone frame を取り出します。

func centralManager(
    central: CBCentralManager,
    didDiscoverPeripheral peripheral: CBPeripheral,
    advertisementData: [String : AnyObject],
    RSSI: NSNumber)
{
    let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID : NSData]
    
    if let serviceData = serviceData {
        let eddystoneServiceData = serviceData[CBUUID(string: "FEAA")]
    }
}

こんな感じで、アドバタイズメントデータから `CBAdvertisementDataServiceDataKey` をキーとしてサービスデータを取り出し、その中からさらに Eddystone Service UUID をキーとして Eddystone フレームデータが取り出せます。

フレームタイプを判別する

Eddystone フレームの先頭バイトが Eddystone のフレームタイプを示します。

class func frameTypeForEddystoneServiceData(data: NSData) -> EddystoneFrameType {
    
    var bytes = [UInt8](count: data.length, repeatedValue: 0)
    data.getBytes(&bytes, length: data.length)
    let firstByte = bytes[0]
    
    if firstByte == 0x00 {
        return .UID
    }
    else if firstByte == 0x10 {
        return .URL
    }
    else if firstByte == 0x20 {
        return .TLM
    }
    return .Unknown
}

(Enum定義は省略)

こんな感じで、UID / URL / TLM を判別します。

各フレームデータのパース

ここは細かい話になるので、詳細は割愛しますが、実装にあたっては下記のソースコードを参考にしました。

  • Google のサンプルコード
    • 本家のコードなので仕様の解釈に困ったらこちらが指針になる
    • あくまでサンプル。あまり綺麗に、使い回しやすくは書かれていない。。
    • Eddystone-URLフレームタイプは非サポートPull Request 送信済み

距離推定(近接度推定/レンジング)

iBeacon と同等に使おうと思うと、アドバタイズメントデータ受信時に呼ばれる `centralManager:didDiscoverPeripheral:advertisementData:RSSI:` の第4引数に入ってくる RSSI 値と、UID あるいは URL フレームに入っている TxPower より距離を推定するなり、RSSI 値を閾値処理して近い/遠い(iBeacon でいう Proximity)を判別するなりする必要があります。


これについては色々とあるのでまた別記事に書きたいと思います。

バックグラウンドサポート

Eddystone向けの特別な手順は必要ありません。普通のセントラル側のバックグラウンド対応と同様です。詳しくは冒頭で紹介した参考書籍等をご参照ください。

TLM / URL フレームの取り扱い

TLM / URL フレームは UID フレームと違って UUID/Major/Minor に相当するような「ビーコンを特定するデータ」を持っていません。が、たとえばTLMフレームからメンテナンス用の情報を取得できたとして、「このTLMデータはどのビーコンのものなの?」というのを解決しないと意味がありません。


ではどうするのかというと、ペリフェラルの UUID を用いて UID フレームと紐付けます。


TLM フレームは Frame Specification に書かれている通り、UID や URL フレームの合間にアドバタイズされるものなので、

  • UIDフレームデータを保持するモデルクラスに、TLMデータを保持できるようプロパティを用意しておく
@interface EddystoneUIDBeacon : EddystoneBeacon

@property (nonatomic) NSNumber *txPower;
@property (nonatomic) NSString *namespaceId;
@property (nonatomic) NSString *instanceId;

@property (nonatomic) EddystoneTLMBeacon *tlmBeacon;

@end
  • UIDフレームが検出されたら、ペリフェラルUUIDをキーとして、UIDフレームデータをDictionaryに保持する
beaconDic[beacon.identifier] = foundBeacon
  • TLMフレームが検出されたら、ペリフェラルUUIDをキーとして上記Dictionaryの値を取り出し、値(=UIDフレームデータ)があればそのプロパティにTLMデータをセットする
foundBeacon = beaconDic[beacon.identifier]
foundBeacon.tlmBeacon = beacon


といった感じで実装できます。


じゃあURLの場合はどうするのかというと、Googleのサンプルには実装されていなかったので、正式なところはよくわかりません。URLにID的なものを持たせられる、という考え方なのかもしれません。


が、自分が今回試していた kontakt.io のビーコンでは、UID/URL/TLMの3パケットを順番にアドバタイズする(設定切り替えによってではなく)という挙動だったので、TLM同様ペリフェラルUUIDを用いてUIDフレームと紐付ける、という実装を行いました。(Google に送った Pull Request もそういう実装)

まとめ

今回は実装編として、Eddystone ビーコンを iOS アプリから検出する際の実装方法をポイントをかいつまんで紹介しました。Eddystone は Bluetooth SIG に承認されないと使用できない16ビットUUID を持っているとか、TLMはUID/URLフレームの合間にアドバタイズされるものであるとか、TLMフレームはUIDフレームと紐付けて用いるとか、(今回は書きませんでしたが)URLを20バイト以内(実質18バイト以内)にどう収めているか等々、個人的には実装してみるまでは見過ごしていた仕様が多々ありました。今回割愛した、「RSSIからの距離あるいは近接度推定」についてはiBeaconと共通の話でもあるので、また別記事で書きたいと思います。


(追記)書きました: http://d.hatena.ne.jp/shu223/20151203/1449097048