Edited at

RubyMotionでiBeacon

More than 5 years have passed since last update.

こんばんは、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' を追加します。


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端末を使うと良いでしょう。

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

# -*- 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ライブラリが登場すれば一気に便利になりそうなので、誰か書かないですかねぇ(お前が書け?)