こんばんは、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の概要については @u_akihiro さんが今日のハッカソンで分かりやすい資料で紹介されていたので、そちらをご覧ください。
20131217 i beaconハッカソン
http://www.slideshare.net/reinforcelab/20131217-i-beacon
サンプルプロジェクトの作成
それでは、iBeaconを試すためのサンプルプロジェクトをRubyMotionで作ってみましょう。今回は「HelloBeacon」という名前でプロジェクトを作成しました。
% motion create --template=ios HelloBeacon
iBeaconはCoreLocationに含まれているので、プロジェクトのRakefileのframeworksに 'CoreLocation' と 'CoreBluetooth' を追加します。
# -*- 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端末を使うと良いでしょう。
実際に発信をおこなうコードは以下のようになります。
# -*- 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
このコードでポイントは下記の部分です。
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を通じておこないます。実際のコードは以下のようになります。
# -*- 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
少し長いので順を追ってポイントを見ていきましょう。
region = CLBeaconRegion.alloc.initWithProximityUUID(UUID, identifier: "com.example.region")
発信側のリージョンではUUIDとmajor/minorを指定していましたが、受信側のリージョンでは指定していません。これは発信側を店舗に据え付けられた専用機器、受信側を顧客がもつiPhoneにインストールされたアプリという典型的なユースケースを想定しています。もちろん、受信側でもmajor/minorを指定することは可能です。
@manager = CLLocationManager.alloc.init
@manager.delegate = self
@manager.startMonitoringForRegion(region)
CLLocationManagerのオブジェクトを生成して、自分自身をdelegateに設定し、最後にstartMonitoringForRegionでiBeaconのリージョン監視の開始を要求しています。
def locationManager(manager, didStartMonitoringForRegion: region)
manager.requestStateForRegion(region)
end
リージョンの監視が開始されたタイミングで requestStateForRegion を呼び、開始時のステートを確認します。
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
領域の内に入った場合、領域の外に出た場合のdelegateでそれぞれレンジングの開始と終了を要求します。
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ライブラリが登場すれば一気に便利になりそうなので、誰か書かないですかねぇ(お前が書け?)