寝過ごし防止アラーム - Kii Cloud で"プチ"スマートハウス

Kii Cloud と ESP8266(ESP-WROOM-02 開発ボード)を使った寝過ごし防止アラームを開発しました。

リビングルームの状態を光センサーでキャッチし、寝過ごしたと思われる場合にだけスマートフォンの目覚まし機能が動作します。

GitHub にソースコード(Android 側ESP8266 側)を公開しています。また、 Amazon.co.jp アソシエイトで関連商品を紹介していますので、調達などの参考にしてください。製作過程、最終成果物ともに、怪我、装置の破損、感電、火災などのリスクが伴います。参考にする場合は全て自己責任でお願いします。

インテリジェントな目覚まし時計

普通の目覚まし時計は決まった時間にアラームを鳴しますが、「寝過ごし防止アラーム」は寝過ごした場合にだけ通知します。活動中と判断できる場合は通知をスキップします。

普段とは違う目覚まし音を使うことで、遅刻に対する緊急事態を知らせるのが目的です。

  • リビングルームには ESP8266 と光センサーを置いておきます。リビングルームの照明が点灯しているときは「起きている」と判断します。

  • ベッドルームには「寝過ごし防止アラーム」がインストールされたスマートフォンを置いておきます。

  • 起床予定時刻+αになると、アプリが自動起動し、Kii Cloud 経由でリビングルームの状態を確認します。30 秒以内に一定以上の照度が確認できない場合、寝過ごしたと見なしてアラームを鳴らします。

残念ながら、普段から遅刻寸前で活動している α≒ 0 な人は、これを使っても遅刻は避けられないです…。

デモ

こちらは動画による動作デモです。

  • 無事に起きられた場合:リビングのセンサーが明るい状態で起床予定時刻+αを迎えると、そのまま終了します。

  • 寝過ごした場合:センサーにカバーを掛けてリビングが真っ暗な状態を再現しています。30 秒後にアラームが鳴ります。

仕組み

今回も Kii Cloud の IoT 向け機能「Thing Interaction Framework」を使ってソリューション全体をまとめます。

Android では、設定した時刻になったらアプリを自動起動します。アプリからは Thing Interaction Framework のコマンドを送信し、ESP8266 に届けます(図の 1~2)。ESP8266 は、光センサーの値を読み取り、値が一定以下ならエラー、一定以上なら正常のコマンドリザルトを返します(図の 3 ~ 4)。Android では、コマンドリザルトのエラーで明るさを判断し、必要時にのみ音を鳴らします(図の 5)。

Thing Interaction Framework のコマンドは、パラメーター付きのアクションを複数持つことができますが、今回は空のアクション checkSensorAction だけを使って、計測のタイミングだけを通知するものとします。

Kii Cloud 経由にした理由

システム構成上、直接接続できる距離なのに Kii Cloud 経由でインターネットアクセスしているのは、信頼性を考慮してのことです。

経験上、スマートフォンでは、Wi-Fi や Bluetooth で近くのデバイスにアクセスする方が、Web ブラウザーでのインターネットアクセスよりもエラーが起こりやすいイメージです(基本機能ですから…)。より安定した動作が見込めるインターネット経由の方を選びました。

Kii Cloud の 24 時間運用の信頼性も、犬用トイレ使用中センサー で実証済みです。何度、夜中に犬のトイレで起こされたことか…。

ハードウェア

まずは、ハードウェアの準備です。

今回は、ESP-WROOM-02 開発ボードを使うことにしました。ESP8266 とその周辺回路がセットになっている製品で、電源の自作を行わなくても、USB 電源で運用できます。開発当時は、Kii Cloud に接続できるメジャーなマイコンとして最安でしたが、現在は Raspberry Pi Zero W という選択肢もあります。

ESP-WROOM-02 は PC と接続して Arduino の開発環境から開発できます。プログラムが書き込んでテストしたら、PC から外し、適当な USB 電源につないで 24 時間運用できます。

今回は、ベースとなるハードウェアに、CDSセル(光センサー)、抵抗、LED を組み込みました。全体の回路は以下のような感じです。

実験段階では評価ボードを使うのが一般的ですが、私は常時運用が目的なので半田付けしてしまいました。邪道かもしれませんが、IC ソケットを 2 階建てにして、周辺回路を取り外せるようにしています。

回路には、照度計測用の光センサーの他、動作チェック用の LED を組み込んでいます。この種のデバイスはいったん起動すると、動いているかどうかも判断できなくなりますので、いわゆる「L チカ」によって動作状態を出力します。

ESP-WROOM-02 を使った開発環境の構築や、LED と光センサーを使う方法は、DEVICE PLUS さんの 第34回 Arduinoマイコンとしても使える小型WifiモジュールESP-WROOM-02を使ってみる(Arduino利用編) が参考になると思います。

MQTT

今回ポイントは、ESP8266 側の通信を MQTT だけで構築している点です。

犬用おやつあげ機 では Thing-IF SDK を組み込んでいましたが、Arduino 環境には Thing-IF SDK を簡単に適用できそうにないので SDK を使わずに実装します。

Thing Interaction Framework は MQTT プロトコルだけで Thing 側(モノ=デバイス)を構築できます。

Thing 側でサーバーからのコマンドを受信する時と、Thing 側からサーバーの API を呼び出す時の両方で MQTT プロトコルを使用します。MQTT のコネクションを確立し、サーバーからの PUBLISH コマンドの到着を待ちます。さらに、サーバーの API を呼び出すときは、クライアントから PUBLISH コマンドを送信します。

HTTP / HTTPS の併用が不要なので、思っていた以上にクライアント側の実装がシンプルになりました。

なお、ESP8266 での MQTT プロトコルの利用は、ESP8266でSangoにパブリッシュしてみる あたりが参考になると思います。

ESP8266 側のプログラム

ESP8266 側は、Arduino の実装モデルのとおり loop() 関数にコードを記述していきます。

loop() 関数は短い時間で何度も呼び出されるため、内部状態を管理することで、以下の処理ステップを少しずつ進めます。

  1. Wi-Fi への接続 Wi-Fi を初期化します。初期化できれば次へ。

  2. デフォルト MQTT ブローカーで初期登録 デフォルト MQTT ブローカーに接続し、初期登録のリクエストを行って API 実行用の MQTT ブローカーの接続情報を取得します。情報が取得済みになったら次へ。

  3. API 実行用の MQTT ブローカーでコマンドを受信 API 実行用の MQTT ブローカーに接続し、コマンド受信を知らせる PUBLISH コマンドを待ちます。2 のコネクションは不要なので閉じます。

負荷分散のため、初期登録用のサーバーと、実際のリクエストを受け付けるサーバーは別になっているようです。初期登録を行った際に得られる API 実行用のサーバーに向けて、MQTT コネクションを張り直す必要があります。

実行のシーケンスは以下のとおりです。

接続に失敗した場合は、同じ処理を繰り返しますが、「指数関数的後退」に従って、再試行までの待ち時間は前回までの倍になるようにしています。ループ時に LED を点滅するように実装することで、点滅間隔からエラーの有無を知ることができます。

以下、プログラムの実装イメージを紹介します。GitHub のコードではクラスでラップしたり関数化したりしていますが、実装はこれと同じです。

Wi-Fi への接続

Wi-Fi への接続処理は次のような感じです。

WiFi.begin() で指定された SSID に接続した後、ステータスが WL_CONNECTED になるまで待ちます。SSID はコード上の固定値(#define)です。

ステータスが WL_CONNECTED の場合は接続済みなので、次のステップに進みます。

void setup() {
  WiFi.mode(WIFI_STA);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
      delay(WAIT_TIME);
    }
  } else {
    // Wi-Fi接続完了
  }
  delay(WAIT_TIME);
}

デフォルト MQTT ブローカーで初期登録

次に、デフォルト MQTT ブローカーに接続して、API 実行用 MQTT ブローカーへの接続情報を取得します。

MQTT の実装には、PubSubClientが提供している PubSubClient クラスを使います。

接続時のユーザー名などのパラメーターは、Kii Cloud のアプリケーションの AppID や AppKey から機械的に決まります。

接続後は、PUBLISH コマンドを送信して初期登録を行います。初期登録での THING_VENDOR_ID(vendorThingID)と THING_PASSWORD はデバイスごとに異なる値が必要ですが、ここでは #define での固定値としています。

callbackGetEndpoint() 関数は、サーバー側からの PUBLISH コマンドを受け取ったときに実行される関数です。set_callback() メソッドで PubSubClient に設定します。

#include <PubSubClient.h>

void loop() {
  ......
  if (mqttClientPrimary == NULL) {
    // デフォルトMQTTブローカーに接続
    String user = "type=oauth2&client_id=" KII_APP_ID;
    String password  = "client_secret=" KII_APP_KEY;
    String clientID = "anonymous";
    mqttClientPrimary = new PubSubClient(wifiClient, "mqtt-jp.kii.com");
    mqttClientPrimary->connect(MQTT::Connect(clientID).set_auth(user, password));
    mqttClientPrimary->set_callback(callbackGetEndpoint);

    // 初期登録を実行
    String topicName = "p/anonymous/thing-if/apps/" KII_APP_ID "/onboardings";
    String publishPayload = String("POST\r\n"
                                   "Content-type: application/vnd.kii.OnboardingWithVendorThingIDByThing+json\r\n"
                                   "\r\n"
                                   "{\"vendorThingID\" : \"") + String(THING_VENDOR_ID) + String("\", \"thingPassword\" : \"") + String(THING_PASSWORD) + String("\"}");
    g_mqttClientPrimary->publish(topicName, publishPayload);
  }
  ......
}

void callbackGetEndpoint(const MQTT::Publish& pub)
{
  String json = pub.payload_string();

  // JSONを解析
  StaticJsonBuffer<1000> jsonBuffer;
  JsonObject& root = jsonBuffer.parseObject(json);

  // API実行用MQTTブローカーの接続情報を取得
  String host = root["mqttEndpoint"]["host"];
  String user = root["mqttEndpoint"]["username"];
  String password = root["mqttEndpoint"]["password"];
  String topic = root["mqttEndpoint"]["mqttTopic"];
  String accessToken = root["accessToken"];
  String thingID = root["thingID"];
}

PUBLISH コマンドを受け取ったら、ArduinoJson(JSON 解析ライブラリー)によってペイロードの JSON を解析し、API 実行用 MQTT ブローカーの接続情報を取得します。

PUBLISH コマンドのペイロードは、以下のような JSON です。上のコードの通り、必要な箇所の情報を取得します。

{
  "accessToken" : "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "thingID" : "th.xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx",
  "mqttEndpoint" : {
    "installationID" : "xxxxxxxxxxxxxxxxxxxxxxxxx",
    "username" : "xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxx",
    "password" : "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "mqttTopic" : "xxxxxxxxxxxxxxxxxxxxxxx",
    "host" : "jp-mqtt-xxxxxxxxxxxx.kii.com",
    "portTCP" : 1883,
    "portSSL" : 8883,
    "portWS" : 12470,
    "portWSS" : 12473,
    "ttl" : 2147483647
  }
}

API 実行用の MQTT ブローカーでコマンドを受信

次に、新しいコネクションとして API 実行用の MQTT サーバーに接続して、サーバー側からの PUBLISH コマンドを待ちます。

先ほどと同様、PubSubClient クラスを使います。接続時のパラメーターは、先ほどの callbackGetEndpoint() 関数で取得した値です。

接続後は、サーバーからの PUBLISH コマンドをコールバック関数 callbackMain() で待ちます。

#include <PubSubClient.h>

void loop() {
  ......
  if (mqttClientMain == NULL) {
    mqttClientMain = new PubSubClient(wifiClient, host);
    mqttClientMain->connect(MQTT::Connect(topic).set_auth(user, password));
    mqttClientMain->set_callback(callbackMain);
    mqttClientMain->subscribe(topic);
  }
  ......
}

void callbackMain(const MQTT::Publish& pub)
{
  String json = pub.payload_string();   // アプリからのコマンド
  // 解析&センサー値読み取り処理
  mqttClient->publish(MQTT_TOPIC_COMMAND_RESULT, /* コマンドリザルトのJSON */);
}

callbackMain() で PUBLISH コマンドを受け取った際は、光センサーの値を読み込み、結果をコマンドリザルトの PUBLISH コマンドで返します。

ペイロードにはコマンド ID が含まれるため、ArduinoJson によって取得します。

上のコードでは省略されていますが、光センサーの 4 回分の平均値がしきい値以上かどうかを判断して、コマンドリザルトのエラーコードに埋め込んで返しています。

処理の詳細は GitHub のコードを参照してください。

スマートフォン側のプログラム

スマートフォン側は一般的な Android の目覚まし時計アプリと同様の実装です。アクティビティとサービスを使って機能を実現します。

Thing Interaction Framework のコマンドを投げる部分は、犬用おやつあげ機 と同じです。

今回は、CheckSensorAction というアクションクラスを作って、それを postNewCommand() メソッドで送信しています。

KiiAPI api = new KiiAPI(mAdm, mThingIFAPI);
List<Action> actions = new ArrayList<>();
actions.add(new CheckSensorAction());
mAdm.when(api.postNewCommand(actions)
).then(new DoneCallback<Command>() {
  // Succeeded
}).fail(new FailCallback<Throwable>() {
  // Failed
});

コマンドを送信後、ESP8266 からコマンドリザルトが登録されるのを待ちます。取得のタイミングは、FCM によるプッシュ通知とポーリングで制御しています。

以下のコードは、コマンドリザルトの取得処理です。CheckSensorAction の結果が成功したかどうか(光センサーの値が一定以上かどうか)を判断してアプリの状態遷移を行います。

ここでは、コマンドリザルトの result.succeeded で照明のオン、オフを判断します。照明オンの場合は、KII_API_STATE.COMPLETED で完了状態にします。照明オフの場合は、KII_API_STATE.RETRY_SEND_COMMAND で、再度コマンドを投げ直してタイムアウトまで待機します。

タイムアウトとなる 30 秒が経過しても照明のオンが検出できなかった場合(ESP8266 につながらなかった場合も含む)、アラーム音の鳴動を開始します。

KiiAPI api = new KiiAPI(mAdm, mThingIFAPI);
mAdm.when(api.getCommand(mLastCommand.getCommandID())
).then(new DoneCallback<Command>() {
  @Override
  public void onDone(Command command) {
    CheckSensorActionResult result = (CheckSensorActionResult) command.getActionResult(new CheckSensorAction());
    if (result != null) {
      if (result.succeeded) {
        mApiState = KII_API_STATE.COMPLETED;
      } else {
        mApiState = KII_API_STATE.RETRY_SEND_COMMAND;
        // ......
      }
    }
    // .....
  }
}).fail(new FailCallback<Throwable>() {
  // Failed
});

利用しての感想

実は、このソリューションを作ったのは 8 ヶ月前でした。それ以降、寝過ごしが発生した際には、普段とは違うアラーム音で無事に起きることに成功しています。

私が勤務している Kii 株式会社では勤務時間の厳守よりも、業務が滞りなく進むことが重視されるため、重要度は下がるのですが、妻の会社用には実用的だと感じているところです。

将来のスマートハウスでは、照明の電源スイッチや動体センサーなどと連動して目覚まし時計が起動することになるかもしれませんが、今回の仕組みだけでも、かなり使えるソリューションだと感じています。

余談ですが、一度アラームが誤動作したことがありました。起きているのにアラームが鳴るバグか!? …と思ったら、なぜかデバイスの上にクマのぬいぐるみのお尻があったのでした。