iBeaconをmyThingsのトリガーに利用する

  • 12
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

iBeaconとmyThingsを連携させる具体例として、iBeaconのビーコン領域に入ったり出たりした際に、myThingsにトリガーをかけるiOSアプリを作ってみようと思います。
iOSアプリとmyThingsを連携させている例があまりないので詳しめに。

予備知識

myThingsとIDCFチャンネル

myThingsやIDCFチャンネルについてはmac008008さんがまとめられた記事「myThingsとIDCFチャンネルでできること」が参考になります。

iBeacon

アップルはiBeaconについてこう言っています。

iBeacon は iOS の位置情報サービスを拡張する新しいテクノロジーです。iOS デバイスは、iBeacon の設置場所に近づいたり離れたりした時点で、App に通知することができます。位置のモニタリングのほか、App は iBeacon (たとえば、Apple Store 直営店のディスプレイやレジカウンター) との距離を測定することもできます。iBeacon では、位置情報を緯度と経度ではなく、BLE (Bluetooth Low Energy) 信号を利用して割り出します。この信号を iOS デバイスが検出します。

iOS:iBeacon について - Apple サポート

iBeaconと呼ぶ場合、技術の名前を指すのかiBeaconの信号を出す機器を指すのかがわかりづらいため、本稿では前者をiBeacon、後者をビーコンと呼び分けることにします。なお、iBeaconにおけるビーコンは自身の識別情報(UUID, メジャー番号, マイナー番号等)をBLEの電波で発信し続ける装置です。

iOSの位置情報サービス(CoreLocationフレームワーク)には「領域」という概念があります。領域の種類は2つあり、従来から存在していた地理的領域と、iBeaconの登場とともに追加されたビーコン領域に分けられます。

  • 地理的領域 - 任意の緯度経度を中心とした半径nメートルの円で表現される領域
  • ビーコン領域 - ビーコンが発する電波の強さをもとに決まる仮想的な3次元領域(空間の大きさの計算式は非公開)

iOSデバイスが領域に入ったり、領域から出たりするのをiOSが監視することを「モニタリング」と呼んでいます。モニタリング中はiOSによる監視の結果、領域への出入り(横断)があればアプリにそのことを通知します。
アプリインストール直後などではiOSはアプリが監視したい領域を知らないので、「この領域をモニタリングしてください」とあらかじめお願いして覚えてもらう必要があります。一度覚えてもらえばアプリがバックグラウンド状態でも、Appスイッチャーに無くてもこの通知が行われます。

今回はこの通知を利用して、ビーコンの領域に入ったらmyThingsにトリガーをかけるアプリを作ります。

構成

今回やりたいことである「ビーコンの領域に入ったらmyThingsにトリガーをかける」を以下のシステムで実現します。作るものはiOSアプリです。

system.png

処理の流れはざっと以下の通り。

  1. iOSアプリがビーコンを検知
  2. iOSアプリがIDCFチャンネルサーバーにトリガーがかかったことを通知する
  3. myThingsサーバーが15分間隔、あるいは手動による任意タイミングでトリガーの監視を行う
  4. トリガーがかかっていたらmyThingsサーバーがアクションを実行する

アプリからmyThingsに直接トリガーをかけることはできないのでIDCFクラウド上のIDCFチャンネルサーバーを経由します。トリガーをかける側であるiOSアプリと、トリガーを受ける側であるmyThingsのやりとりの仲介を、IDCFチャンネルサーバー上で動いているMeshbluが行うためです。

参考

「IDCF」チャンネルは、IoTのためのメッセージングプラットフォームのMeshbluを使いやすくするためにDocker Composeにまとめています。MeshbluはOctobluがオープンソースで開発をしています。MQTT、HTTP REST、WebSocket、CoAPなど複数のプロトコルを使って相互にブリッジすることができます。例えばRaspberry PiからMQTTでpublishして、ブラウザからWebSocketでsubscribeするといった使い方ができます。MQTTのsubscribeに対してREST APIからメッセージをPOSTすることもできるので、さまざまなデバイスやサービスを連携することができます。
myThingsのはじめかた | IDCFクラウド

準備

必要なもの

  • インターネット接続環境
  • iOSアプリ開発環境
  • サンプルアプリ実行用のiOSデバイス(iOS8以降、BT4.0対応)
  • ビーコン(iOSデバイスがもう1台あればそれで代用可能)

今回作るサンプルアプリを試すにはiBeacon信号を発するビーコンが必要になります。手持ちのビーコンがある場合はその設定情報を利用できます。ビーコンを持っておらずiOSデバイスがもう一台用意できる場合は、ビーコン信号を出すアプリを自作するか、AppStoreでダウンロードして使いましょう。
ビーコン信号を出すだけなら「Beacon Manager」あたりがシンプルでオススメです。

やっておくこと

myThingsのはじめかた | IDCFクラウド」に沿ってサーバー環境の構築とmyThingsアプリでのIDCFチャンネル認証を済ませておいてください。
詳しくない方は割と大変ですが頑張って下さい。Meshblu?Docker?デバイス?のようにわからない言葉だらけでも、読み飛ばさずに上から順番にすすめてゆくことが超重要です(経験者談)。

iOSアプリの実装

iOSアプリでは「ビーコン領域に出入りしたら、IDCFチャンネルに通知する」という処理を行いますが、「ビーコン領域に出入りした」処理と「IDCFチャンネルに通知する」部分を分けて説明します。

今回作るアプリはiOS8以降で動くものとします(iOS7と共存できない部分もあるため)。
サンプルアプリは iOS 9.1 + Xcode 7.1.1 で動作確認しています。

アプリのソースコードはGitHubに置いてありますのでダウンロードしてお使いください。
https://github.com/calmscape/TriggerBeacon

ビーコン処理の実装

位置情報サービスの認証関連処理

アプリからビーコンの検知を行うにはCoreLocationフレームワークを使用します。
位置情報サービスを利用するために、

  • 位置情報サービスの利用目的をInfo.plistに記述
  • ユーザーに対する位置情報サービスの利用許可の問い合わせ

を行う必要があります。


位置情報サービスの利用目的をInfo.plistに記述する

こちらの記事がわかりやすいです。
iOS 8から位置情報を取得する方法が変わるよ - Qiita

今回はバックグラウンドでも位置情報を利用したいので、Info.plist の NSLocationAlwaysUsageDescriptionキーに利用目的の文字列を記述しておきます。

locationservice.png


ユーザーに対する位置情報サービスの利用許可の問い合わせ

CLLocationManagerをインスタンス化した後でrequestAlwaysAuthorizationを呼んでおきます。
バックグラウンドでも位置情報を使いたいという意思表示です。

ViewController.m
if ([CLLocationManager locationServicesEnabled]) {
  _locationManager = [[CLLocationManager alloc] init];
  _locationManager.delegate = self;

  [_locationManager requestAlwaysAuthorization];
}

利用認証が未決定の場合には位置情報サービスの認証アラートが表示されます。オプトイン後は何も表示されません。

ビーコン領域のモニタリング

ビーコン領域を指定してモニタリングを開始します。
今回は、指定したUUIDと同じビーコン領域に入ったら通知することにしています(major番号とminor番号は無視する)。
複数のビーコン領域をモニタリングしたい場合は領域ごとにstartMonitoringForRegion:を呼ぶ必要があります。

ViewController.m
if ([CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]]) {
  [self p_stopMonitoringForAllRegion];

  NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:kBeaconUUID];
  CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid
                                                              identifier:kBeaconRegionIdentifier];
  [_locationManager startMonitoringForRegion:region];

  /*  locationManager:didDetermineState:forRegion:は領域を横断した時にしか呼ばれないので、
   *  領域内でアプリを起動すると呼ばれない。
   *  上記のような場合に領域に入った時の処理を行わせたい場合にはrequestStateForRegion:で
   *  状態の取得要求を行う。
   */
  [_locationManager requestStateForRegion:region];
}

OSがバックグランドでビーコン領域の監視を行い、領域への出入りがあるとアプリに知らせてくれる(起こしてくれる)ので何もしなくてもバックグラウンドで動作します。モニタリングを開始する際に指定するビーコン領域をOSが記憶してくれるので、Appスイッチャーからアプリを消し去っても通知がきます。

ビーコン領域の横断検知

ビーコン領域に入った場合にトリガーをかけるコードは以下のようになります。
複数のビーコン領域を監視している場合は、どの領域に入ったかを特定するためにidentifierプロパティを比較します。

ViewController.m
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
  if (state == CLRegionStateInside) {
    // 領域に入った

    // ビーコン領域によってトリガーを変えるような場合はidentifierで領域を判別する
    if ([region.identifier isEqualToString:kBeaconRegionIdentifier]) {
      // トリガーをかける
    }
  }
  ...
}

ビーコン領域から出た場合には、stateがCLRegionStateOutsideでこのデリゲートメソッドが呼ばれます。
なお、領域に入る場合は数秒で入ったことを検知しますが、領域から出た場合は数十秒かかります。

IDCFチャンネルへの通知

IDCFチャンネルサーバー上で動くMeshbluにメッセージを送ることで、IDCFチャンネルを経由してmyThingsにトリガーをかけることができます。
メッセージの送り方やデバイス情報の取得方法はこちらの記事「myThingsをはじめよう - Part6: 「IDCF」のトリガーからHTTPで「Gmail」のアクションを実行する」が参考になります。
サンプルアプリではIDCFチャンネルのtrigger-1を使ってトリガーをかけるので、前述の記事を参考にtrigger-1デバイスのuuidとtokenをメモしておいてください。

今回はAFNetworkingによるHTTP通信でメッセージ送信処理を行いますが、iOS9で動かす場合はApp Transport SecurityによりデフォルトでHTTP通信ができないため、以下のようにInfo.plistにキーを追加してとりあえず回避しておきます(HTTPS通信できるようちゃんと実装した方が良いですね)。

ats.png

トリガーのお作法

Meshbluのドキュメントによると、/data/{uuid}という形式でポストするとセンサーデータが(データベースに)保存されると書かれています。本来はセンサーデータ等をロギングするために使われるようですが、IDCFチャンネルのトリガーを発火させる場合はこの送信方法を用います。
myThings側は15分毎あるいは手動実行のタイミングでこのデータベースを参照し、トリガーの判定を行っているのではないかと思います。

HTTP POST例

curl -X POST -d "wind=12&temperature=78" https://meshblu.octoblu.com/data/0d3a53a0-2a0b-11e3-b09c-ff4de847b2cc --header "meshblu_auth_uuid: {my uuid}" --header "meshblu_auth_token: {my token}"

これと同じ送り方になるようAFNetworkingで実装します。
"wind=12&temperature=78" というようなデータも送ることができますが、今回はトリガーをかけたいだけなのでダミーのデータを渡しておきます。

URLは http://host/data/{trigger-1 uuid} 、認証情報はHTTPヘッダの以下フィールドにJSON形式で指定します。
meshblu_auth_uuid: trigger-1のuuid
meshblu_auth_token: trigger-1のtoken

これらを実装したコードです。

MeshbluTransmitter.m
/// HTTP POSTでMeshbluにデータを保存する
- (void)postStoreData:(NSDictionary *)parameters triggerUUID:(NSString *)triggerUUID triggerToken:(NSString *)triggerToken success:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock
{
  NSDictionary *postParameters = parameters ? parameters : @{@"foo": @"bar"};

  NSString *httpURLString = [NSString stringWithFormat:@"http://%@", _host];
  NSURL *hostURL = [NSURL URLWithString:httpURLString];
  AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:hostURL];

  manager.requestSerializer = [AFJSONRequestSerializer serializer];
  [manager.requestSerializer setValue:triggerUUID forHTTPHeaderField:@"meshblu_auth_uuid"];
  [manager.requestSerializer setValue:triggerToken forHTTPHeaderField:@"meshblu_auth_token"];

  [manager POST:[@"/data" stringByAppendingPathComponent:triggerUUID]
     parameters:postParameters
        success:^(AFHTTPRequestOperation *operation, id responseObject) {
          ...
        } failure:^(AFHTTPRequestOperation *operation, NSError *error){
          ...
        }];

}

自分のサーバー環境に合わせて設定を変更する

ViewController.m の頭にある定数宣言をサーバー環境に合わせて変更してください。

ViewController.m
/// 領域監視を行うビーコンのUUID文字列
static NSString * const kBeaconUUID = @"33100867-20AE-4FC1-917C-7A5310DF81D8";

/// 領域の識別子(領域ごとにユニークな文字列ならなんでもよい)
static NSString * const kBeaconRegionIdentifier = @"net.calmsacpe.33100867-20AE-4FC1-917C-7A5310DF81D8";

/// IDCFサーバーのIPアドレス
static NSString * const kHost = @"xxx.xxx.xxx.xxx";

/// 使用するトリガーのUUID(myThingsでIDCFチャンネルのtrigger-1をトリガー条件に指定しているならtrigger-1のuuid)
static NSString * const kTriggerUUID = @"353b5062-bce7-465a-822d-5f4df068edbe";

/// 使用するトリガーのトークン(myThingsでIDCFチャンネルのtrigger-1をトリガー条件に指定しているならtrigger-1のtoken)
static NSString * const kTriggerToken = @"10f3d2b4";

テスト

あらかじめmyThingsアプリでトリガーとアクションを設定しておきます。
この例ではトリガーはIDCFチャンネルのtrigger-1を、アクションはSlackの指定チャンネルにメッセージを投稿するようにしました。

IMG_0204.PNG

次にiOSのサンプルアプリをビルドして実機で起動後、位置情報を利用許可の状態にしておきます。
この状態でビーコンの電源を入れる等して監視しているビーコンの領域に入ると、トリガーがかかったことがIDCFチャンネルに通知されます。

IMG_0205.PNG

15分待つか、myThingsアプリで組み合わせの手動実行を行うとアクションが実行されるはずです。この例ではSlackにメッセージが投稿されました。

ビーコンを使う利点

ビーコンを純粋に位置情報サービスの拡張デバイスとして利用すると、今回のサンプルのようにビーコンの設置場所の近くに来た/遠ざかったことを検知してmyThingsにトリガーをかけるような使い方になります。これはビーコン領域を特定の場所に紐付けた使い方です。

トリガーとなる知識をアプリから分離して仮想領域を定義する

センサービーコンのようにビーコンとセンサーを組み合わせ、たとえば明るくなったら信号を発するようなビーコンを作れば、このビーコン領域は設置場所によって「朝を迎えたベランダ」や「明かりが着いた部屋の中」「扉が開いた冷蔵庫の近く」といった意味に変化します。場所ではなく状態に着目すれば「朝になった」「明かりがついた」「冷蔵庫の扉が開いた」というトリガーが作れることになります。
温度センサーと湿度センサーの情報を利用して「熱中症の危険度がとあるレベルを超えた」というような複雑な条件でもマイコンを組み合わせれば可能ですし、条件を満たした時にビーコン信号を出す(ビーコンに給電する)のは簡単に実現できるでしょう。
ビーコンを持った人がiOSデバイスに近づいたらトリガーがかかるというような使い方も考えられます。
(スピーカーのDockに挿しっぱなしのApple Music専用端末や店頭のデモ機なんかのように常時給電されているところは狙い目ですね)

myThingsからはトリガーがかかったか否かの2値の状態しか見えませんが、ビーコン領域を仮想領域と捉え、その意味づけを変化させることでトリガーの種類を増やすことができるわけです。その場合でもアプリ側は常に「ビーコン領域に入った(出た)らIDCFチャンネルにトリガーをかける中継役」だけをしていれば良いので実装がシンプルになり、トリガー条件の知識をビーコン側に完全に分離できます(トリガーとなるデバイスを置き換えるという発想)。

このようなことがビーコンを使うメリットではないかと考えています。

おわりに

ビーコン領域に入ったことをiOSデバイスで検知し、myThingsのIDCFチャンネル経由でトリガーをかける方法について説明しました。

ビーコンの領域に入った/出たという二値情報は、myThingsのトリガー条件として扱う場合に直感的に理解できます。位置情報サービスとしてのiBeaconですが、「仮想的な位置情報」をどう定義するかで用途の幅が広がります。変更が容易な自作ハードウェアととても相性が良さそうです。