RubyMotionでiBeacon

こんばんは、RubyMotionで一番良く打つコマンドは `motion update` の @mackato です。

今日は岐阜県大垣市のドリームコアで行われたiBeaconハッカソンに参加していたので、RubyMotionでiBeaconを使う方法について紹介します。

## iBeaconとは

iBeaconとはBluetooth Low Energyを利用した位置と近接の検出技術です。iBeaconを利用すると、低消費電力なBluetooth Low Energyの発信機を店舗やミュージアムなどの現実世界に置いて、その近くに来たことをiOSアプリがバックエンドで検出できるようになります。

Bluetooth Low EnergyはiPhone 4S、iOS 5からサポートされていますが、iBeaconはiOS 7から利用できるようになりました。iBeaconの概要については [&#64;u_akihiro](https://twitter.com/u_akihiro) さんが今日のハッカソンで分かりやすい資料で紹介されていたので、そちらをご覧ください。

**20131217 i beaconハッカソン**
http://www.slideshare.net/reinforcelab/20131217-i-beacon

## サンプルプロジェクトの作成

それでは、iBeaconを試すためのサンプルプロジェクトをRubyMotionで作ってみましょう。今回は「HelloBeacon」という名前でプロジェクトを作成しました。

```sh
% motion create --template=ios HelloBeacon
```

iBeaconはCoreLocationに含まれているので、プロジェクトのRakefileのframeworksに 'CoreLocation' と 'CoreBluetooth' を追加します。

```ruby:Rakefile
# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project/template/ios'

begin
  require 'bundler'
  Bundler.require
rescue LoadError
end

Motion::Project::App.setup do |app|
  # Use `rake config' to see complete project settings.
  app.name = 'HelloBeacon'
  app.frameworks += %w(CoreLocation CoreBluetooth)
end
```

以上でiBeaconに対応したプロジェクトの準備ができました。それでは、実際にiBeaconのやりとりをおこなってみましょう。

## iBeaconの発信をおこなう

実際に店舗等で運用する場合はボタン電池などで動作する安価な専用機器を発信に使うことが多いと思いますが、iOSデバイス自体を発信に利用することができます。専用機器を用意するのは敷居が高いと思うので、iBeaconの動作確認などには手持ちのiOS端末を使うと良いでしょう。

実際に発信をおこなうコードは以下のようになります。

```ruby:
# -*- coding: utf-8 -*-
class HomeViewController < UIViewController
  UUID = NSUUID.alloc.initWithUUIDString("YOUR_UUID_HERE")

  def viewDidLoad
    super
    view.backgroundColor = UIColor.whiteColor

    @region = CLBeaconRegion.alloc.initWithProximityUUID(UUID, major: 1,
                                                               minor: 2,
                                                               identifier: "com.example.region")
    @manager = CBPeripheralManager.alloc.initWithDelegate(self, queue: Dispatch::Queue.main.dispatch_object)
  end

  def peripheralManagerDidUpdateState(peripheral)
    if peripheral.state == CBPeripheralManagerStatePoweredOn
      @manager.startAdvertising @region.peripheralDataWithMeasuredPower(nil)
    end
  end
end
```

このコードでポイントは下記の部分です。

```ruby:
CLBeaconRegion.alloc.initWithProximityUUID(UUID, major: 1,
                                                 minor: 2,
                                                 identifier: "com.example.region")
```

UUIDはアプリケーションやサービスを識別するための128-bitの識別子です。フォーマットは決まっていて、グローバルにユニークである必要があるので、Macの `uuidgen` コマンドで生成するのが良いです。

major/minorは16ビットの任意の番号を使用できます。例えば小売店で利用するアプリであれば、店舗毎にmajorを、売り場毎にminorを使い分けるというのが典型的な使い方です。

## iBeaconの受信をおこなう

iBeaconの受信はGPS位置情報の受信等をおこなうCLLocationManagerを通じておこないます。実際のコードは以下のようになります。

```ruby:
# -*- coding: utf-8 -*-
class BeaconViewController < UIViewController
  UUID = NSUUID.alloc.initWithUUIDString("YOUR_UUID_HERE")

  def viewDidLoad
    super

    self.view.backgroundColor = UIColor.whiteColor

    @label = UILabel.alloc.init
    @label.textAlignment = NSTextAlignmentCenter
    @label.text = "Waiting..."
    @label.frame = [[0, 100], [320, 100]]
    self.view.addSubview(@label)

    region = CLBeaconRegion.alloc.initWithProximityUUID(UUID, identifier: "com.example.region")

    @manager = CLLocationManager.alloc.init
    @manager.delegate = self
    @manager.startMonitoringForRegion(region)
  end

  def locationManager(manager, didStartMonitoringForRegion: region)
    manager.requestStateForRegion(region)
  end

  def locationManager(manager, didDetermineState: state, forRegion: region)
    if state == CLRegionStateInside
      manager.startRangingBeaconsInRegion(region)
    end
  end

  def locationManager(manager, didEnterRegion: region)
    if region.isKindOfClass(CLBeaconRegion)
      manager.startRangingBeaconsInRegion(region)
    end
  end

  def locationManager(manager, didExitRegion: region)
    if region.isKindOfClass(CLBeaconRegion)
      manager.stopRangingBeaconsInRegion(region)
    end
  end

  def locationManager(manager, didRangeBeacons: beacons, inRegion: region)
    beacon = beacons.last

    if beacon
      proximity = case beacon.proximity
                  when CLProximityUnknown
                    "Unknown"
                  when CLProximityFar
                    "Far"
                  when CLProximityNear
                    "Near"
                  when CLProximityImmediate
                    "Immediate"
                  else
                    "Nothing"
                  end
      @label.text = "#{proximity}: #{beacon.major}-#{beacon.minor}"
    end
  end
end
```

少し長いので順を追ってポイントを見ていきましょう。

```ruby
region = CLBeaconRegion.alloc.initWithProximityUUID(UUID, identifier: "com.example.region")
```

発信側のリージョンではUUIDとmajor/minorを指定していましたが、受信側のリージョンでは指定していません。これは発信側を店舗に据え付けられた専用機器、受信側を顧客がもつiPhoneにインストールされたアプリという典型的なユースケースを想定しています。もちろん、受信側でもmajor/minorを指定することは可能です。

```ruby
@manager = CLLocationManager.alloc.init
@manager.delegate = self
@manager.startMonitoringForRegion(region)
```

CLLocationManagerのオブジェクトを生成して、自分自身をdelegateに設定し、最後にstartMonitoringForRegionでiBeaconのリージョン監視の開始を要求しています。

```ruby
def locationManager(manager, didStartMonitoringForRegion: region)
  manager.requestStateForRegion(region)
end
```

リージョンの監視が開始されたタイミングで requestStateForRegion を呼び、開始時のステートを確認します。

```ruby
def locationManager(manager, didDetermineState: state, forRegion: region)
  if state == CLRegionStateInside
    manager.startRangingBeaconsInRegion(region)
  end
end
```

開始時に領域内にいる場合はレンジング（距離測定）の開始を要求します。

```ruby
def locationManager(manager, didEnterRegion: region)
  if region.isKindOfClass(CLBeaconRegion)
    manager.startRangingBeaconsInRegion(region)
  end
end

def locationManager(manager, didExitRegion: region)
  if region.isKindOfClass(CLBeaconRegion)
    manager.stopRangingBeaconsInRegion(region)
  end
end
```

領域の内に入った場合、領域の外に出た場合のdelegateでそれぞれレンジングの開始と終了を要求します。

```ruby
def locationManager(manager, didRangeBeacons: beacons, inRegion: region)
  beacon = beacons.last

  if beacon
    ...
    @label.text = "#{proximity}: #{beacon.major}-#{beacon.minor}"
  end
end
```

レンジングの結果をdelegateで受け取ってlabelに表示しています。beaconはCLBeaconクラスのオブジェクトです。CLBeaconからはmajor/minorの他、proximity(発信側とのおおよその近接関係)、accuracy(精度)、rssi(電波強度)などが取得できます。

実際のアプリケーションではmajor/minorを判別して処理を分岐させたり、サーバーサイドにmajor/minorを送信するような使い方になると思います。また、proximityやrssiは電波状況などの環境に左右されて精度はあまり高くないので活用には注意が必要なようです。

## まとめ

RubyMotionはiOSの薄いWrapperなので、iBeaconのような最新のOSの機能もObjective-Cで開発するのと変りなく実装することができましたが、このままではあまりRubyMotionっぽくないですね。

RubyMotion用のiBeacon Wrapperライブラリが登場すれば一気に便利になりそうなので、誰か書かないですかねぇ（お前が書け？）