犬用トイレ使用中センサーの製作

前回の 犬用遠隔おやつあげ機 に続いて、今回は犬用のトイレ使用中センサーを紹介します。犬がトイレを使うと、Kii Cloud 経由でスマートフォンにトイレ周辺に犬がいることが通知されます。

例によって動画を用意しました。動画では撮影用にトイレを撤去し、センサーの真下でゴハンを食べてもらって、スマートフォンに通知が届く様子を撮影しました。

仕組み

今回も Android スマートフォン、Windows PC(IoT ゲートウェイ代わり)、Arduino(ハードウェア制御用)の組み合わせで実現します。Kii Cloud を使用してスマートフォンと Windows PC を連携させます。

構成は次のような感じです。

距離センサーには、Arduino で実績がある、赤外線方式の距離センサー GP2Y0A21 を使いました。このセンサーは赤外線を使って、対象物までの距離を 10cm ~ 80cm の範囲で計測できます。

運用時にはトイレ付近のケージの屋根にセンサーを置き、地面までの距離を測ります。通常、センサーは 80cm 以上(測定上限)の値を示しますが、犬がセンサーの下に入ると 20cm ほど小さな値が出るので、犬がいることを検知できます。実験から、75 cm以下になったらスマートフォンにプッシュ通知を送ることにしました。

スマートフォンへのプッシュ通知は、Android 標準の GCM(Google Cloud Messaging)を、GCM のアプリケーションサーバには Kii Cloud を使用します。

全体の流れは次のような感じです。

  • IoT デバイス側

    Arduino と Windows PC が連携して距離を測定し、75 cm 以下になったらスマートフォンにプッシュ通知を送ります。Arduino では赤外線センサーの値を読み取って長さに変換し、Windows PC に送ります。Windows PC では長さを集計し、トイレ使用中と判断したら Kii Cloud にプッシュ通知を送ります。

  • スマートフォン側

    プッシュ通知を初期化して、メッセージが届くのを待ちます。プッシュ通知が届いたら、それを画面(Android のステータスバー)に表示します。

Thing-IF SDK と Thing SDK Embedded

前回 のブログでは Thing-IF を使って迅速に IoT のソリューションを実現できたので、今回も Thing-IF を使いたいところですが、残念ながらそれはできません。Thing-IF では、特定のシナリオを想定した機能モデルが用意されており、用途が限られています。

トイレ使用中センサーで必要となるシナリオは、「IoT デバイス側でイベントが起こったとき、スマートフォンにコマンドを送る」というものですが、これはまだカバーされていません。

ありがちな高水準の SDK では、非サポートの機能が出てきた途端に全く使えないフレームワークと化してしまうのですが、Kii Cloud には回避手段があります。Thing-IF が内部で使用している低水準の SDK(Thing SDK Embedded)を利用できるため、Thing-IF と併用して開発の迅速性と機能の柔軟性を両立できます。

つまり、利用できる部分は高水準の API(Thing-IF)を使って素早く実装し、サポートしていない機能は低水準の Thing SDK Embedded を使って思い通りに実現するといった使い分けができます。

Thing-IF SDK(高水準) Thing SDK Embedded(低水準)
開発速度 極めて早い 早い(自分でサーバを用意するより)
応用範囲 狭い(特定シナリオのみ) 広い(機能要素を組み合わせて構築)

なお、Thing SDK Embedded の使用方法はまだドキュメント化されていませんが、オープンソースのため簡単な解析で利用できます。今回のブログの中で少し紹介していきます。

Kii Cloud によるプッシュ通知

Kii Cloud では、3 種類のプッシュ通知機能が用意されていますが、今回は最も汎用性が高い Push to User と呼ばれる機能を利用します。

仕組みは次の図の通りです。

Windows PC は Kii Cloud から見て IoT デバイスとして見えます。Kii Cloud では、IoT デバイスを「Thing」(つまり「モノ」)と呼んでいます。

  1. Windows PC に対応する Thing スコープの領域にトピック TopicDistanceAlert を作成します。
  2. トピックをスマートフォンの操作ユーザーから購読します。
  3. Thing からトピックにメッセージを送ると、購読しているユーザーのデバイスに GCM 経由でプッシュメッセージが届きます。

なお、Thing と操作ユーザーの間には、オーナー関係が必要です。これによってセキュリティを実現しています(関係ないデバイスにアクセスできないように)。今回はおやつあげ機で Thing-IF を使った際に、自動的にオーナー関係が構築されているため、デバイスの登録やオーナーの設定を省略することができます。

この 1 ~ 3 の動きを IoT の制御プログラムとアプリの上に構築すれば、トイレ使用中の通知を受け取ることができます。

Thing 側の実装

Thing 側では、Arduino で地面までの距離を測定し、それを COM ポート経由で Windows PC に送り続けます。Windows PC ではそれを集計して、犬が下に来たと判断したタイミングでトピックにメッセージを送って通知を行います。

距離の計測

まずは、犬がセンサーの下に入ったかどうかを検知しなければなりません。赤外線距離センサーで 75 cm 以下になるタイミングを待ちます。

赤外線距離センサーを Arduino から使う方法は、ネット上で多く紹介されていますが、簡単に触れておきます。今回は、戸田よろず研究所さんが公開されている こちら のプログラム例を使わせていただきました。

データシートによると、計測した距離の値と電圧値は図のような関係で計測できるので、Arduino のアナログポートの値を読み取ります。

図の通り、センサー値と距離の関係は曲線で表現する必要があるのですが、今回は犬が下に来たかどうかが分かればよいので、線形の近似値で十分です。以下はリンク先のページから抜粋したコードのイメージです(実際にはセンサーの 200 回分の平均値から距離値を計算しています)。

#define ANALOG_PIN_DISTANCE 0
#define COEFF1 0.004883
#define COEFF2 20.0
#define COEFF3 0.30

int sensor = analogRead(ANALOG_PIN_DISTANCE)
float distance1 = COEFF2 / (COEFF1 * (float)sensor - COEFF3);

Arduino からは、distance1 の値を一定間隔(約 0.5 秒間隔)で COM ポートに送出します。

犬が下に来たかどうかの判断

Windows PC では Arduino から送られてきた距離値を集計します。

Arduino では常に距離値を計測して、200 回の平均値を出力していますが、単純に「距離値が 75 cm 以下になったときプッシュ通知」としてしまうと、以下のようなノイズがあったとき、プッシュ通知が何度も行われてしまいます。

これでは困るので、フィルタリングを行います。ハードウェアからソフトウェアまでの全過程で、以下の処理を行っています。

  • ハードウェアレベルでは、電源ラインに 100μF の電解コンデンサを挟んで電気的なノイズを除去します。
  • Arduino 側で 200 回の平均値を計算して安定化します(だいたい 0.5 秒間隔の値になります)。
  • Windows では、Arduino から送られてきた値が 6 回連続で 75 cm を下回ったときに条件成立と見なします。途中で 75 cm 以上になるとカウントをリセットします。
  • プロセス起動から 10 秒、前回送信時刻から 60 秒が経過していないときはプッシュ通知を抑制します(起動直後の不安定な時期や、連続的なプッシュ送信は回避)。

プッシュ通知のタイミングを判断できたら、Kii Cloud のトピックにメッセージを送信します。

今回は犬がトイレを使い出した瞬間に通知が欲しいので、検出が遅すぎても、ノイズが多すぎても問題があります。実際の犬にも何度か協力してもらいながら、パラメータやアルゴリズムを調整してチューニングしました。例えば水を飲みに行くのにセンサーの近くを通った場合、ノイズが出ることがありますが、これらはできるだけ通知しないようにチューニングしました。

使用しているセンサーや要件によっては、別のアプローチが必要かもしれません。例えば、センサーが直流的なノイズの影響を受けて断続的に値が上下するとか、小刻みな変化を素早く捕らえたい場合もあります。このような場合は、距離の絶対値ではなく前後の相対関係を見るようにしたり、立ち上がりと立ち下がりでスレッショルドとなる値を使い分けるなどの工夫が必要になるでしょう。

プッシュメッセージの送信

Windows PC でプッシュ通知を送信するには、トピックを作成する処理と、そのトピックにメッセージを送信する処理の 2 つが必要です。

トピックの作成は、初期化処理の最後で実行しています。Thing スコープのトピック TopicDistanceAlert を作成する処理は次のような実装になります。

#define TOPIC_NAME_DISTANCE "TopicDistanceAlert"
string restVendorThingID = string("VENDOR_THING_ID:") + config->GetVendorThingID();
kii_topic_t topic = {
  KII_SCOPE_THING,
  restVendorThingID.c_str(),
  TOPIC_NAME_DISTANCE
};
err = kii_push_create_topic(&m_kii, &topic);
if (err != 0) {
  return FALSE;
}

この処理は、Thing SDK Embedded を直接呼び出すことで実現しています。2016 年 2 月現在ではまだドキュメント化されていない部分ですが、SDK のソースを解析すると、kii_push_create_topic 関数にトピックの作成処理が実装されていることが分かります。

パラメータとして、スコープ、スコープ内の対象 ID(今回は Thing スコープなので Thing の vendorThingID)、トピック名を指定しています。なお、この処理を通すには SDK のソースコードの調整も必要です(kii_topic_t の char* を const char* に変更)。

メッセージの送信処理

トピックが作成できたら、そのトピックにメッセージを送信します。

困ったことに、メッセージの送信機能は Thing SDK Embedded にも実装されていないのですが、特定の REST API を呼び出す機能が実装されているので、それを利用します。

実装は次のような感じです。

BOOL KiiCloudClient::SendPushToDistanceAlert()
{
  Configuration* config = Configuration::GetInstance();
  string resourcePath = string("/api/apps/") +
                        config->GetAppID() +
                        string("/things/VENDOR_THING_ID:") +
                        config->GetVendorThingID() +
                        string("/topics/" TOPIC_NAME_DISTANCE "/push/messages");
  const char* messageBody = "{\"data\": {}, \"sendToProduction\": true, \"gcm\": {\"enabled\": true}, \"apns\": {\"enabled\": false}}";
  if (kii_api_call_start(&m_kii, "POST", resourcePath.c_str(), "application/vnd.kii.SendPushMessageRequest+json", KII_TRUE) != 0) {
    return FALSE;
  }
  if (kii_api_call_append_body(&m_kii, messageBody, strlen(messageBody)) != 0) {
    return FALSE;
  }
  if (kii_api_call_run(&m_kii) != 0) {
    return FALSE;
  }
  return TRUE;
}

公式ドキュメント から、REST API によるプッシュメッセージの送信機能を調べると、次のような CURL コマンドが出てきます。

curl -v -X POST \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  -H "X-Kii-AppID: {APP_ID}" \
  -H "X-Kii-AppKey: {APP_KEY}" \
  -H "Content-Type: application/vnd.kii.SendPushMessageRequest+json" \
  -H "Accept: application/vnd.kii.SendPushMessageResponse+json" \
  "https://api-jp.kii.com/api/apps/{APP_ID}/things/VENDOR_THING_ID:{VENDOR_THING_ID}/topics/{TOPIC_NAME}/push/messages" \
  -d '{"data": {}, "sendToProduction": true, "gcm": {"enabled": true}, "apns": {"enabled": false}}'

C のコードは、この CURL コマンドを Thing SDK の内部 API に置き換えたものです。まず、kii_api_call_start で HTTP ヘッダ部分を作成し、次に kii_api_call_append_body で HTTP BODY を連結します。最後に kii_api_call_run を呼び出せば、内部で REST API を呼び出して実行結果が得られます。

以上が IoT デバイス側の主要な部分です。次はスマートフォン側でこれを受信する部分の実装です。

スマートフォン側の処理

Android スマートフォン側の実装では、メッセージハンドラでプッシュ通知の受信処理を実装します。

GCM の設定や初期化が必要ですが、公式ドキュメント のチュートリアルに沿って準備しておきます。

GCM が使えるようになったら、初期化処理とメッセージの受信処理を実装します。

初期化処理

まず、Windows PC 側で作成したトピックをログイン中のユーザーから講読します。

トピックの講読は次のような感じです。初期化ごとに Thing スコープのトピックを再作成して講読します。本当はトピックが作成 / 講読済みかどうかをデバイス上に保存しておくとよいのですが、とりあえず、うちの家族しか使わないので実用には問題ない範囲で実装しています。ConflictException を無視すれば重複登録時のエラーをスキップできます。

KiiTopic topic = thing.topic("TopicDistanceAlert");
try {
  topic.save();
} catch (ConflictException ce) {
  // ignore the conflict
}

KiiPushSubscription sub = KiiUser.getCurrentUser().pushSubscription();
try {
  sub.subscribe(topic);
} catch (ConflictException ce) {
  // ignore the conflict
}

メッセージの受信処理

メッセージの受信処理は GCM の GcmListenerService の拡張クラスで次のメソッドをオーバライドして実装します。実装方法は GCM のサンプルコードから引用してきました。

トピックからのメッセージ受信は PUSH_TO_USER で判別できるので、次のようにトピック名を確認して onDistanceAlert に分岐させます。トピック名は Windows PC で #define TOPIC_NAME_DISTANCE をしたものと同じです。

@Override
public void onMessageReceived(String from, Bundle data) {
  ReceivedMessage message = PushMessageBundleHelper.parse(data);
  KiiUser sender = message.getSender();
  PushMessageBundleHelper.MessageType type = message.pushMessageType();
  switch (type) {
    ...
    case PUSH_TO_USER:
      PushToUserMessage userMsg = (PushToUserMessage) message;
      KiiTopic topic = userMsg.getKiiTopic();
      if (KiiAPI.TOPIC_NAME_DISTANCE_ALERT.equals(topic.getName())) {
        onDistanceAlert();
      }
      break;
      ...
  }
}

onDistanceAlert() の実装は次のような感じです。うちの犬の名前は「さくら」なので、タイトルを「さくら活動中」としてステータスバーに通知を出すようにしています。本当はこのあたりもコンフィグ化/国際化しないといけないのですが、使うのはうちの家族だけなので…(略)。

private void onDistanceAlert() {
  Intent intent = new Intent(this, MainActivity.class);
  intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);

  Bitmap bmp = null;
  try {
    bmp = ImageLoader.getBitmapFromURL(ViewLivePCFragment.getLivePCUrl(getApplicationContext()) + "images/image2.jpg");
  } catch (Exception e) {
    // ignore the exception, bmp = null
  }

  Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
  NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
      .setSmallIcon(R.mipmap.ic_launcher)
      .setContentTitle("さくら活動中")
      .setContentText("何かしているみたい。")
      .setAutoCancel(true)
      .setSound(defaultSoundUri)
      .setContentIntent(pendingIntent);
  if (bmp != null) {
    notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(bmp));
  }

  NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
  notificationManager.notify(0, notificationBuilder.build());
}

動かしてみる

というわけで、アプリを起動し、Windows PC の監視プロセスを立ち上げておくと、犬がトイレを使ったときに次のような通知が飛んでくるようになりました。

今回はステータスバーにメッセージを表示するようにしています。スマートウォッチ Android Wear では、ステータスバーの通知を自動的に転送できるため、次のように時計にも通知が入ります。

ついでに宣伝。私が作った Wear カスタムバイブ を使うと、通知ごとに異なるバイブレーションをスマートウォッチで鳴動させるようにカスタマイズできます。こんな感じで設定しておけば、画面を見なくても 1.7 秒の長いバイブ 3 回で犬がトイレを使っていることを認識できます。

まとめ

長々と説明しましたが、Kii Cloud SDK を直接使うと IoT デバイスからのプッシュ通知をスマートフォンで受け取るという流れも比較的簡単に実現できます。

将来、Thing-IF で利用できるシナリオが増えた場合はこのブログの情報も不要になるはずですが、こんな感じで IoT を使う場合にも低レベルの API を組み合わせていろいろできるので、柔軟性と開発の迅速性を両立できるのは重要な特徴だと思います。

ちなみに、完成したソリューションは、うちの家族の間で実運用中です。先日、午前 3 時にうちのわんこがトイレを使ったため、スマートフォンに起こされました。Kii Cloud、ちゃんと 24 時間安定稼働している証拠ですね。