ゆべねこの足跡

主にVR、Unity、Arduinoについて書いていきます。

UnityのC#でインターフェースを使う

本ブログ初のVRコンテンツに関する内容です。春休み中に設計に関する技術書を読んだので実践してみようと思い作ってみました。

読んだ本はこちらです

Adaptive Code ? C#実践開発手法 第2版

テストやリファクタリングの仕方からSOLID原則、依存性の注入など大規模な開発に不可欠な要素が学べる素晴らしい書籍となっておりました。特にインターフェースの有効性について学べたのが一番良かった点ですね。

というわけで、今回は「インターフェースを利用した抽象化をしてみる」という目的でVRコンテンツを作ってみました。

成果物

コントローラーの加速度を読み取って魔法を放出し敵を倒すゲームとしました。

クラス図

f:id:yubeshineko:20190406115208p:plain

今回が初めてなので正直なところ書き方がこれで合ってるかは分からないですが書いてみました。注目点はインターフェースや抽象クラスを使った抽象化です。将来的にどのポイントが拡張されるかを考えてそこを拡張できるように抽象化しました。例をあげるとプレイヤーの入力をIInputProviderインターフェースとして抽出してあげることでテスト入力がしやすくなったり違うコントローラーでも使えるようにしました(今回はテスト入力用のクラスは作りませんでしたが...)。Leap Motionを使った何かをやってみたかったのでいずれ暇があればハンドジェスチャで魔法を飛ばしたりできるようなIInputProviderインターフェースの実装をしてみたいと考えていたりもしてます。

他にはタグの代わりにインターフェースを使うということもやってみました。ダメージを与える対象のオブジェクトにIDamageApplicableインターフェースを実装したスクリプトをアタッチして与ダメージオブジェクトが被ダメージオブジェクトに衝突した際に与ダメージオブジェクトが衝突したオブジェクトからIDamageApplicableインターフェースを取得します。取得できたら被ダメージオブジェクトのApplyDamage()を実行といった形になります。開発の最初の段階でダメージを与える際のメソッドの引数はこうだよ!とインターフェースを指定しておくことで敵の種類が増えた時に引数が増えてしまい上位モジュール(今回の場合、Magicクラス)に変更が生じる...なんてことがなくなります。これはSOLID原則の「依存性反転の原則」, 「オープン/クローズド原則」を守るための工夫でもあります。今回の例で考えるとMagicオブジェクトがSkeletonオブジェクトに衝突した際にMagicクラスがGetComponent<IDamageApplicable>()を実行し、取得できたらSkeletonオブジェクトのApplyDamage()を実行するといった感じですね。

void OnTriggerEnter (Collider other)
{
    var enemy = other.gameObject.GetComponentInParent<IDamageApplicable> ();
    if (enemy != null)
    {
        enemy.ApplyDamage (damage);
    }
}

感想

今回初めてまともに設計してみたんですが、やっぱり最初は難しいですね。特に名前空間の名前決めや各クラスをどの名前空間に所属させるかなどは悩みどころでした。もしかしたらゲームのジャンルごとにある程度テンプレート的なものがあったりするのでしょうか? こればっかりは慣れるしかないのでしょうね。

それと、最初の段階でどこまで拡張できるようにするかをしっかり決めることも大事です。最初の段階ではIDamageApplicableインターフェースは以下のようになっていました。

namespace WCE.Damages
{
    public interface IDamageApplicable
    {
        void ApplyDamage(Attribute attribute);
    }
}

これは魔法の属性だけで敵に与えるダメージ計算をすることを想定したインターフェースでした。しかし、開発の途中で「プレイヤーがアイテムを拾って攻撃力が上がったら?」 なんてことを考えた結果、開発の途中でインターフェースを書き換えるという禁忌を犯すことになりました。これはSOLID原則の1つであるオープン/クローズド原則に違反する行為であり、あってはならない事態でした。今後はこのようなことがないよう初めの段階でキチンと設計をしておくようにしたいです。

あと、モチベーションが続くボリュームにすることが大事ですね。開発当初は敵の種類や魔法の属性をたくさん考えていたりプレイヤーのステータス上昇アイテムなどを考えていたのですが、途中で心がポッキリ折れてしまったので結局遊べるところまで持っていって終わろうということにしました。インターフェースで拡張ポイントは確保してあるのでまたやる気が出たら拡張することはできるんですけど当分はやれそうもありません。

それでも「インターフェースを使って抽象化をする」ことは実践できたので満足ではあります。

LINE Clovaに地域のゴミ出しの方法をしゃべってもらう

スマートスピーカーっていいですよね。スマホみたいに画面を開いたりすることなく情報をインプットできるので。

今回はとあるハッカソンでLINE Clovaを取り扱うことがあったのでその時に作ったClovaのスキルについて書いていきます。

システム構成

f:id:yubeshineko:20190225152208p:plain

Clovaに知りたいゴミの捨て方を聞くとその情報が返ってくるという感じですね。ゴミの捨て方のデータは福島県会津若松市のオープンデータである会津若松市 家庭ごみ分別辞典から持ってきました。バックエンドで使用した言語はC#です。

スロットとインテントの設定

スロットとインテントの設定は以下のようにしました。スロットとインテントの詳細については割愛します。

  • スロット f:id:yubeshineko:20190225160004p:plain 49個全てをUIから入力するのは大変なので、tsv形式のデータを用意して読み込ませました。

  • インテント f:id:yubeshineko:20190225160215p:plain

また、終了に関するワードを取り扱うためにTerminateスロットとTerminateインテントも用意しました。

...Garbageのスペル間違ってた!

スクリプト

残念ながらハッカソンの時間内では複数検索結果が見つかった場合の処理は実装できませんでした。実装するとしたら、LINEのGUIで選ばせるという方法が考えられますね。

使用したパッケージ

いづれVSCode内にてNugetを通して導入できます。なお、LINE botと連携するにはClovaスキルとMessaging APIのプロバイダーが同じである必要があります。

動作の様子

f:id:yubeshineko:20190225163338p:plain

写真なのでわかりにくいのですが、ちゃんと要求したゴミの情報を汲み取ってくれてClovaが捨て方について話してくれます。また、長い情報で聞き逃してしまっても大丈夫なようにLINEにも通知が飛んでくれます。

参考文献

マンガでわかるLINE Clova開発 1話「どんな仕組みで動くの? サーバーレスって何?」|TECH PLAY Magazine[テックプレイマガジン]

ハッカソンで大失敗した話を晒す

2月下旬に某所で開かれた某ハッカソンに参加したのですが、そこでハッカソンで考えられる全ての失敗を犯したと思うので、今後の自分の戒めとして記録に残しておこうと思います。

開発しようと思ったコンテンツ

VRゲームってプレイヤーだけしか楽しめないじゃないか...

そんな思いからこんなコンテンツを考えつきました。

センサを装備したプレイヤーが何かしらのジェスチャーをすることでVR空間内にジェスチャーに対応する変化が現れる...みたいな。例えば三人のプレイヤーがいるとすると...

  1. プレイヤー1の前にゴブリンがあらわれた!
  2. ゴブリンの色が赤(炎属性)だ!
  3. プレイヤー1には炎属性のゴブリンに対応する武器が必要だ!
  4. プレイヤー1がプレイヤー2とプレイヤー3に武器(水の剣)を要請
  5. センサを持ったプレイヤー2とプレイヤー3はそれぞれ剣を生成するジェスチャ、水属性を付与するジェスチャを実行
  6. プレイヤー1の上空から水属性が付与された剣が降ってくる
  7. プレイヤー1が剣を手にとってゴブリンを倒す!

というような流れを実装したいと考えていました。

チームメンバー

私は敵周りおよびUnityとマイコンのパイプ部分を担当しました。

技術的仕様

  • Unityのバージョン : 2018.3.5f1
  • HMD : HTC Vive
  • マイコン : Arduino Mega
  • センサ : 加速度センサとカラーセンサ。前者が武器、後者が属性を表す

具体的なコンテンツ仕様

f:id:yubeshineko:20190222174828p:plain

f:id:yubeshineko:20190222174854p:plain

  • 武器は剣、魔法の杖、銃の3種類(1回きりの使い捨て)
  • 敵はスライム、オーク(白板ではゴブリン)、鳥系の3種類
  • 属性は炎、草、水の3種類(●ケモンとは関係ありません...多分)
  • スライムなら魔法の杖、オークなら剣、鳥系なら銃が対象の武器となる
  • 武器はプレイヤーの座標の上空に降ってくる
  • 敵はそれぞれ特有の移動をする
  • 弱点属性だと一撃で倒せるが、弱点属性でない属性がついた武器ではあまりダメージが入らない
  • プレイヤーはワープでの移動が可能

ということを実装するつもりでした。(雲行きが怪しい...)

結果

大失敗!

今回のこの企画は私が考えたものでしたので、自動的に私がディレクターとして開発を進めることになっていたのですが、私の失敗のためにチームに多大な迷惑をかけることになってしまいました。結果、ハードウェアの部分は完成してUnityに任意のデータを送るまではできたのですが、コンテンツ部分が完成せず、デモができないという結果なってしまいました。ハッカソン終了後に一人反省会をしまして、何がいけなかったのかをしっかり考えてみたのですが、様々なミスを犯していたことに気づきました。

うまくいかなかった原因

  • コンテンツ設計

    • 属性、形状など考慮すべき点がかなり多かったため開発が大変になってしまった

      実は今回のハッカソンが私にとってまともに参加した初めてのハッカソンでした。チームでの開発経験もあまり豊富であるとは言えませんし、ましてや開発の主軸となった経験もありませんでした。そんな状態で決して単純とは言えない今回のコンテンツの仕様を考えてしまったのが失敗の原因その1だと考えられました。もちろん自身の開発力の無さも原因の1つではあるのですが、それを考慮した上で仕様を練ることも必要だったのではないかと思います。

  • チームオペレーション

    • 問題点や進捗の共有をうまく行えていなかった

      一応進捗報告はチームのSlackチャンネルで確認を取っていたのですが、作業をしているうちにその進捗内容が他のメッセージの中に埋もれてしまって出来上がったモデルやアニメーションなどに気が付かなかったなどということがあったりしました。また、メンバーの誰がどんな作業をしているのかが分からなかったということもありました。

      他にも、もう一人のUnityプログラマーが、Unityの最新バージョンにてViveコントローラーの入力の取得方法が今までのバージョンと違い苦労しているということに早く気付くことができず、1日目の深夜(1:00)頃にようやくそのことに気付いてバージョンを下げたなんてこともありました。

      これについてはTrello等でカンバン管理をして進捗の状況把握や問題点の把握をすると良かったのかなという感じです。

  • 開発

    • まともに使ったこともないのにクラスの継承を意識したクラス設計をしてしまい、後半で詰まってしまった

    • クラス設計をおろそかにしてしまい、後半においてゲーム進行のロジック作成時に詰まってしまった

      当初は敵の動きの実装を以下のような感じで実装してみようと考えていました。

      f:id:yubeshineko:20190222195824p:plain

      Slime, Bird, OrcはEnemyBaseを継承しているという形です。

      これで敵単体の動作はできたのですが、問題は属性に関わる部分でした。本来ならば自身の属性を表す変数なりプロパティなりをEnemyBase抽象クラスが持っておくべきだったのですが、それの必要性を後半まで気付くことができませんでした。結果、敵を生成する際のロジック、攻撃された武器の属性判定からのダメージ計算ロジックの設計に戸惑ってしまいました。他にも、GetCompont<EnemyBase>()でちゃんと派生クラスのデータを持ってこれるのかなども分かっていませんでした。このため、敵の生成ロジック記述時に時間を食ってしまいました。

    • 作れもしないのに時間ギリギリまで作業をしてしまって結果的にプレゼンに割く時間を完全に無くしてしまった

      個人的ハッカソンでやってはいけないことワースト1位。終了10分前くらいで既に完成は無理だろうと断言できるような状態だったのですが、結局開発終了までディスプレイとにらめっこしていました。そのため制作物プレゼンテーションで使うスライドの制作がまともにできませんでした。幸いチームメンバーの一人が作ってくれてはいたのですが、その人は他のハッカソンにも参加しており、そちらが忙しくなってしまったので最終的に中途半端なスライドのままプレゼンをすることになり、コンテンツの企画内容や苦労した点、工夫した点などについて十分に伝えることができませんでした。

    • 土台を作ることよりも小さいところに気を払いすぎてコアの部分となるゲーム進行ロジックをまともに作れなかった

      個人的ハッカソンでやってはいけないこと同率ワースト1位。優先順位が狂っていたということですね。1日目の後半で敵の動きの実装をしてる最中にQuaternionで詰まってしまってそこでかなり時間を費やしてしまい、代償としてコアとなるロジックの実装にかける時間を溶かしてしまいました。冷静に考えてみれば、敵の動きの実装なんかよりもゲームマネージャーの実装の方がはるかに重要度は高いことは言うまでもないですよね。

    • 早い段階で妥協点を模索して遊べる段階まで持っていくことが最優先であったはずなのに、最初の仕様に最後までこだわってしまった

      これに関しては、最初の段階で実装でかかる時間や重要度の見積もりをしたり、実装する必要があるものを列挙したりするべきだったと思います。そうすればどの作業を優先的に行えばいいか分かり、あまり重要ではない部分で悩んで時間を取られることはなかったと思われます。他にも、早い段階でコンテンツの複雑さを理解でき、仕様変更することを考えることができたのではないかと思います。

まとめ

審査員の一人がハッカソンの講評でこんなことを述べていました。「ハッカソンでのコツは最初にプレゼン用の資料を作り、チームでどんなコンテンツを作るかを最初に共有して、土台を作ることを最優先でやることだ」と。本当にその通りだと実感しました。細々としたところもそれはそれで重要であるとは思いますが、結局一番大事なのは動くものを見せることなんですよね。今回のハッカソンではそのことを痛感しました。

また、ハッカソンで今までやったことがない新要素を取り入れることはいいことだと考えているのですが、それをするなら仕様は単純の方がいいです(特に今回のような短い期間のハッカソンではなおさら)。切羽詰まって開発することもなくなりますし、デモをする時間も多く取れるようになりますからね。何より、心のゆとりが生まれてハッカソンを楽しむことができるようになります。

この失敗を次の機会に大いに生かしていこうと思います。

UnityからMQTTブローカに接続し、メッセージをUIとして表示させる

MQTTについて色々やっているうちに「UnityからMQTTブローカに接続する方法があるのだろうか」という思いに駆られてしまったので、実践してみることにしました。今回はそのことについて書いていきます。

実行環境

  • Mac OS 10.14.2
  • Unity2018.3.4f1

Unityの.NETのバージョンは4.xとします。

使用するMQTTクライアントライブラリについて

C#用に作られたMQTTクライアントのライブラリにはいくつか種類がありますが、有名どころだとM2MqttかMQTTnetあたりになるようです。双方のGitHubのコミット履歴を見てみたところ、MQTTnetの方が開発がアクティブであったため、今回はMQTTnetを使ってやっていくことにします。

UnityにDLLファイルをインポートする

NuGet GalleryからMQTTnetの最新の安定バージョンである2.8.5をダウンロードします。ダウンロードはこちらから。ダウンロードされたデータ名はmqttnet.2.8.5.nupkgとなっていますが、このままでは展開できないので拡張子を.nupkgから.zipに変えて展開します。展開後、mqttnet.2.8.5 > lib > net472 中にある MQTTnet.dll を使用するUnityプロジェクトのAssets/Plugins/以下にインポートします。これでMQTTnetをUnity内で使用する準備ができました。

実装

この記事での具体的な実装としては、UnityからMQTTブローカに接続、SubscribeしてPublishされたメッセージをUnity内でテキストとして表示させるということをします。テキストの表示にはTextMeshProを利用しました。また、メッセージを受信した際のコールバック(ApplicationMessageReceived)内で受信したメッセージを他クラスに渡せるようにしています(UniRxの機能の一部を利用)。

非同期処理に注意

MQTTController.csの20行目と67行目に注目すると普段あまり見慣れないことが書いてありますね。

var context = SynchronizationContext.Current;

//            ~~~~  略 ~~~~

context.Post (_ =>
            {
                OnMessageReceived.OnNext (message);
            }, null);

実はUnity固有の処理はメインスレッド以外から実行することができません。これに気付かなくて1時間くらい表示されないテキストを前にして唸ってました...

MQTTnetの内部ではたくさんの箇所で非同期処理が行われており、そのほとんどの処理においてconfigureawait(false)となっています(こちらを参照)。結果、MQTTController.cs内ではスレッドIDが刻々と変化してしまい、Unity固有の処理をできなくなる...となるわけです。実験としてMQTTController.csの各所にスレッドIDを表示するログを出力するようにしてみます。

async void Start ()
    {
        var context = SynchronizationContext.Current;
        Debug.Log ($"はじめ : {Thread.CurrentThread.ManagedThreadId}");

        //   ~~~~ 略 ~~~~

        mqttClient.Connected += async (s, e) =>
        {
            Debug.Log ("MQTTブローカに接続しました");
            Debug.Log ($"ブローカ接続後 : {Thread.CurrentThread.ManagedThreadId}");
            await mqttClient.SubscribeAsync (
                new TopicFilterBuilder ()
                .WithTopic ("your/Subscribe/Topic")
                .Build ());
            Debug.Log ("指定したトピックをSubscribeしました");
            Debug.Log ($"Subscribe後 : {Thread.CurrentThread.ManagedThreadId}");
        };

        //   ~~~~ 略 ~~~~

        mqttClient.ApplicationMessageReceived += (s, e) =>
        {
            var message = System.Text.Encoding.UTF8.GetString (e.ApplicationMessage.Payload);
            Debug.Log ($"メッセージ受信 : {message}");
            Debug.Log ($"メッセージ受信後(context.Post以前) : {Thread.CurrentThread.ManagedThreadId}");
            context.Post (_ =>
            {
                Debug.Log ($"メッセージ受信後(context.Post内) : {Thread.CurrentThread.ManagedThreadId}");
                OnMessageReceived.OnNext (message);
            }, null);
        };

結果

f:id:yubeshineko:20190210025651p:plain

確かに、各処理の後においてスレッドIDが異なっていることが分かります。そして、context.Post内ではちゃんとUnityのメインスレッドに戻っていますね。試しにcontext.Postの部分をコメントアウトして実行してみると、テキストが表示されなくなります。

完成品

ちょっとした遊び心で「ぬこ」というメッセージをPublishすると猫が現れるようにしました。

参考記事

MQTTnetをUnityで使えるようにする

MQTTnet を Unity で使う - Qiita

うまくいかなかった原因解明のきっかけ

Oculus GoとNode-REDでMQTTをやり取りするメモ – 1ft-seabass.jp.MEMO

非同期処理とSynchronizationContextについて

【Unity開発者向け】「SynchronizationContext」と「Taskのawait」 - Qiita

Unity2017で始めるTask(async~await) - Qiita

AlexaからESP32につながったLEDを操作した話

去年のアマゾンのサイバーマンデーにてEcho dotが通常価格5,980円のところ3,240円で売られていたのでつい衝動買いしていたことを思い出したのでESP32との連携をして遊んでみることにしました。

目次

実行環境

  • OS : macOS Mojave 10.14.2
  • Arduino IDE : 1.8.5
  • ESP32 : ESPr Developer 32(SWITCH SCIENCE)
  • Echo dot : 第3世代

目標

AlexaからESP32に接続されたLEDを操作する。

f:id:yubeshineko:20190108005339p:plain

準備

ソフトウェア領域

  • ESP32の開発環境
  • ライブラリ各種(下記参照)
  • Alexaのスマホアプリ

ハードウェア領域

  • LED(ここでは赤色を利用しました)
  • 抵抗(LEDの色に合わせた大きさを利用しましょう。ここでは330Ωを利用しました)
  • ESP32
  • Echo dot

使用したライブラリ

FauxmoESPというライブラリを利用させていただきました。私の環境で実行した時のバージョンは3.1.0でした。

xoseperez / fauxmoESP — Bitbucket

READMEを見てみると、依存関係にあるライブラリがあるそうです。ESP8266とESP32で違うそうなので、ESP32用の依存ライブラリも入れておきましょう。

$ cd your_ArduinoDir/libraries
$ git clone https://bitbucket.org/xoseperez/fauxmoesp.git
$ git clone https://github.com/me-no-dev/AsyncTCP.git

コードを書く

ライブラリのサンプルコードを少し変更したものです。

#include <Arduino.h>
#include <WiFi.h>
#include "fauxmoESP.h"

fauxmoESP fauxmo;

const char *ssid = "****************";
const char *password = "****************";
const int ledpin = 26;
const char *id_led = "led";

void WifiSetup()
{
  WiFi.mode(WIFI_STA);
  Serial.printf("connecting to %s¥n", ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.print(".");
    delay(100);
  }

  Serial.println();
  Serial.print("WiFi connected: ");
  Serial.println(WiFi.localIP());
}

void setup() {
    Serial.begin(115200);
    pinMode(ledpin, OUTPUT);
    digitalWrite(ledpin, LOW);
    WifiSetup();

    fauxmo.createServer(true);
    fauxmo.setPort(80);  //※
    fauxmo.enable(true);

    fauxmo.addDevice(id_led);

    fauxmo.onSetState([](unsigned char device_id, const char *device_name, bool state, unsigned char value) {
      Serial.printf("Device #%d (%s) state: %s value: %d\n", device_id, device_name, state ? "ON" : "OFF", value);

      if (strcmp(device_name, id_led) == 0) {
        digitalWrite(ledpin, state ? HIGH : LOW);
      }
    });
}

void loop() {
    fauxmo.handle();
}

fauxmo.setPort(80);のところですが、第3世代のEcho dotを使う場合は使用するポート番号は80番でないといけないようです。

回路を組む

ESP32の26番ピン - LED - 抵抗 - GND となるようにつなぎます。よくあるLEDの回路ですね。

Alexaのスマホアプリを開いてESP32を認識させる

ここでEcho dotに電源を接続してWiFiに接続します。

f:id:yubeshineko:20190107234724p:plain

  1. 下のメニューからデバイスを選択して右上の「+」のアイコンをクリックします。
  2. 下からメニューが出てくるので「デバイスを追加」を選択します。
  3. セットアップするデバイスの種類を選ぶ画面に移動したらすべてのデバイス > その他(一番下にあるのでスクロールします)を選択します。
  4. 「デバイスを検出」をタップします。ここで数秒待ってESP32を検出します。

f:id:yubeshineko:20190107235526p:plain

バイスが検出されたら上の画像のようになるはずです。「デバイスをセットアップ」をタップしてデバイスを登録します。その後、fauxmo.addDevice();の引数として渡した文字列がデバイス名として登録されます。今回は"led"という名前で登録されますね。ここまでやれば作業は終了です。

完成

「アレクサ ledつけて」と言うとつけてくれます。反対に「アレクサ led消して」と言うと消してくれます。

赤外線通信用ライブラリとEcho dot

赤外線通信をして音声で家電操作をしたいと考え、赤外線通信用ライブラリであるIRremoteをこれと試したのですが、実行中にGuru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.などと言うエラーを吐かれてしまいました。espressifの公式サイトによるとこう書かれていました。

This CPU exception happens when application attempts to read from or write to an invalid memory location.

ふむふむ、不適切なメモリ領域にアクセスしてしまい、パニックを起こしていると...。ESP8266とEcho dotの組み合わせで赤外線通信をしている例があったので、どうしても赤外線を飛ばしたいというならそちらを使うか、ライブラリを使わずに赤外線を飛ばすように実装するしかないですね。いづれはこちらも実装してみたいものです。

参考サイト

ESP32をAmazon Alexaのスマートホームのデバイスに登録する方法 - クラクスの記録帳

LINEから家のエアコンを操作する with ESP32

今回はネットワーク上のトリガーによってローカルでアクションを起こすということをやっていきます。ということで、前からずっとやってみたかったLINEから家のエアコンの操作するシステムを作ってみました!

目次

実行環境

システム構成

f:id:yubeshineko:20190105133041p:plain

トリガーとなるワードが送信されたことが確認されたらCloudMQTTにPublishするコードをHerokuにデプロイします。バックエンド側のスクリプトはGoで書きました。

CloudMQTTについて

数あるMQTTブローカのうちの1つ。Herokuのadd onとして利用できるサービスですが、CloudMQTT単体でもサービスを提供しているようです。いくつかの料金プランが用意されていますが、今回は無料プランであるCut Catを使います。無料プランには以下の制限がありますが個人で遊ぶ程度なら十分です。

  • 5 connections
  • 5 users
  • 5 acl rules
  • 0 bridges
  • No kinesis integration
  • API now throttled, 30 req/minute

準備

ソフトウェア領域

  • LINEアカウントとLINE developersへの登録
  • HerokuアカウントとHerokuCLIがインストールされた環境
  • Go言語で開発する環境
  • Arduino IDEのインストール
  • 赤外線データ(データの取得にはこちらを利用しました)
  • Arduinoライブラリのインストール(括弧内は実行時バージョン)
    • PubSubClient(2.7.0)
    • IRremote(2.2.3)

    IRremoteについては本家ではESP32での赤外線送信は対応していないため、こちらの方のものを使わせていただきます。

$ cd your_Arduino_Dir/libraries
$ git clone https://github.com/SensorsIot/Arduino-IRremote.git IRremote

ハードウェア領域

知識

作業

1 LINE チャネル作成と設定。

プロバイダーの作成は省略します。チャネルはMessaging APIとして作成し、プランはDeveloper Trialとします。チャネル基本設定画面まで行ったらChannel Secretアクセストークンをどこかにメモしておきましょう。その後以下の設定をします。

  • メッセージ送受信設定

    • Webhook送信 : 利用する
  • LINE@機能の利用

    • 自動応答メッセージ : 利用しない
    • 友だち追加時あいさつ : 利用しない

    下の2つは必須ではないですが、毎回出てくると鬱陶しいのでこの設定にしておくのがオススメです。

2 Herokuにアプリをデプロイする

このアプリ内ではLINEから送られてきたメッセージの処理とCloudMQTTへのPublishを行います。

2.1 プロジェクトファイル作成
$ cd $GOPATH/src
$ mkdir [アプリ名]
$ cd [アプリ名]
$ pwd
/あなたのGOPATH/src/[アプリ名]
2.2 Herokuへのログインとその他諸々
$ pwd
/あなたのGOPATH/src/[アプリ名]
$ echo 'web: [アプリ名]' > Procfile
$ git init
$ git add .
$ git commit -m “first commit”
$ heroku login
$ heroku create [アプリ名] -b https://github.com/heroku/heroku-buildpack-go.git
$ heroku config:set CHANNEL_SECRET="[Developer用管理画面で取得したChannel Secret]"
$ heroku config:set CHANNEL_TOKEN="[Developer用管理画面で取得したChannel Access Token]"
$ heroku addons:add cloudmqtt

Procfileには、Heroku上で自分のアプリケーションを動かすためのコマンドが記述されます。Buildpackの中で自動的にgoプログラムをgo buildして go install$GOPATH/binにインストールされる仕組みになっているため、web:の後はherokuプロジェクト名を書くのが定石のようです。

ここでさっきメモしたChannel Secretアクセストークンをherokuの環境変数として登録しておきます。また、add on であるCloudMQTTもこの段階で登録しておきます。add onの登録ができたらherokuの環境変数CLOUDMQTT_URLが追加されているので$ heroku config:get CLOUDMQTT_URL環境変数を取得しどこかにコピペしておきます。デフォルトではmqtt://User:Password@Server:Portとなっているはずです。$ heroku addons:open cloudmqttでCloudMQTTのページに飛べるので環境変数と見比べてみてください。

確認すると分かるのですが、デフォルトではポートがMQTTポートになっていますね。コーディングするときのことを考えて、この部分をSSLポートに変えておきましょう。

$ heroku config:set CLOUDMQTT_URL="mqtt://User:Password@Server:SSLPort"
2.3 コードを書く

一応httpで来た時はhttpsにリダイレクトするようにしました。トピックはesp32/airconとしました。

2.4 CA証明書の取得

こちらのページよりPEM形式のルート証明書を取ってきます。テキストエディターにコピーしてca-certificates.crtとして保存しましょう。

2.5 デプロイ

パッケージ依存関係の保存をしてデプロイします。godepをインストールしていない方は$ go get github.com/tools/godepでインストールしましょう。

$ godep save
$ godep go install
$ git add .
$ git commit -m “second commit”
$ git push heroku master

最終的にはこんな構造になります。

$ tree -L 1 .
.
├── Godeps
├── Procfile
├── ca-certificates.crt
├── main.go
└── vendor
2.6 LINEのWebhook URLの設定

https://[アプリ名].herokuapp.com/callbackという形で登録します。さて、ここまで来たら第一段階突破です。友達登録を済ませたらCloudMQTTのWEBSOCKET UIを開きましょう。LINEで「つけて」「けして」と送信してみてCloudMQTTにメッセージがPublishされたら成功です。

3 ESP32のコードを書く


config.h

// Wi-FiアクセスポイントのSSIDとパスワード
const char *ssid = "your_ssid";
const char *password = "your_password";

// クライアントID(任意)
const char *clientID = "Arduino";

//CLOUDMQTT
const char *host = "CloudMQTT_Server";
const char *CloudMQTT_user = "CloudMQTT_User";
const char *CloudMQTT_pass = "CloudMQTT_Password";
const int port = CloudMQTT_SSLPort;

const char *topic = "esp32/aircon";

certificate.h

const char* ca_cert = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU\n" \

 ... 略 ...

"mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=\n" \
"-----END CERTIFICATE-----\n" \
;

CA証明書は先ほどのものと同じものを利用します。しかし、上記のようにArduinoに読み込ませるには少し変換が必要なようです。手打ちでやるのも面倒なのでこちらの方のプログラムを利用させていただき、証明書の変換をします。

最後にコードをESP32に書き込みます。

4 回路を組む

f:id:yubeshineko:20190105175640j:plain

本体にはUSBで給電します。ゲート抵抗は100Ωにしました。赤外線LEDには100mAまで流せるので100mA流れる想定で考えます。赤外線LEDにつなぐ抵抗ですが、赤外線LEDにかかる電圧は1.35~1.6Vらしいので、1.35Vと見積もることにしてオームの法則を適用すると大体15Ωくらいがベストだと考えられます。

完成

ESP32がCloudMQTTに接続している状況でLINEにてメッセージを送るとエアコンがつくようになります。

今後

ESP32を電池で駆動してみたいと考えていますが、どうやらWiFi接続している時はかなり電力を消費するらしいです。センサーの値を一定時間ごとに通知するなどのシステムでは一瞬だけWiFiに接続して後は解除するということができるのですがサーバに接続しっぱなしで電池駆動するとなると半日も持たないかもしれないですね...

ということで、気が向いたらそこらへんの実装をしてみたいと思います。ではでは...

参考サイト

  • Herokuアプリ

GitHub - line/line-bot-sdk-go: Go SDK for the LINE Messaging API

HerokuとGoでLINEの Messaging API環境を作ってみた - Qiita

Documentation - Go | CloudMQTT

MQTT の golang クライアントで TLS 接続を試す - Qiita

Google Home + IFTTT + Beebotte(MQTT Broker) + ESP32でLチカしてみた - Qiita

ESP8266: Connecting to MQTT broker – techtutorialsx

IFTTTのWebhooksをトリガーとしてArduinoから実行する

ArduinoEthernet Shieldを買ってみたのでIFTTTとの連携をしてみようと思いました。最初と最後でハマったので、それについても書いていきます。

目次

使用したもの

全て秋月で購入できるものです。また、Ethernet Shieldを使うにはLANケーブルも必要です。

Maker Channelの代わりにWebhooksを使う

最初のハマりポイント。どこの情報を見てもMaker Channelを使ってやっているんですよね。しかしいくらIFTTTの利用できるサービスで検索してもヒットしない...なんでや...

いろいろ調べてみたら、どうやらMaker Channelの代わりにWebhooksを使うということが判明しました。Webhooksの使い方についてはこちらを見ると分かりやすいかと思います。

回路を作る

今回はタクトスイッチを押したらWebhooksにイベント情報を送って、スマホに通知を送るという流れにします。回路はよくあるタクトスイッチを使った回路です。

f:id:yubeshineko:20180910195928j:plain

アプレットを作る

thisのところをWebhooksにします。イベントの名前は今回はButtonPlessedとでもしておきましょう。thatはNotificationsにします。また、このときにWebhooksのシークレットキーを確認しておきます。確認の仕方はこちらを参考にするとよいです。

コードを書く

Webhooksはウェブリクエストを送ってやるとイベントを発火させられるようです。ここではこちらのサイトのコードを参考にしてタクトスイッチが押されたらGETメソッドを使ってウェブリクエストを行うという処理を書いていきます。MACアドレスEthernet Shieldを買ったときにシールドの裏面についているシールに書かれています。また、コンパイルする際はEthernet2ライブラリのインストールをしてからにしてください。

ちなみに、ArduinoIDEのバージョンは1.8.5です。

#include <SPI.h>
#include <Ethernet2.h>

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 1, 177);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);

EthernetClient client;
char server[] = "maker.ifttt.com";

// IFTTT strings
char eventName[] = "ButtonPlessed"; // イベント名
char secretKey[] = "****************"; // シークレットキー
char s[128];//リクエストメッセージ保持用配列

const int buttonPin = 2;
int lastButtonState = LOW;

void setup() {
  Serial.begin(9600);
  while (!Serial)
  {
  }

  Serial.println("Trying to get an IP address using DHCP");
  if (Ethernet.begin(mac) == 0)
  {
    Serial.println("Failed to configure Ethernet using DHCP");

    Ethernet.begin(mac, ip, gateway, subnet);
  }
  Serial.print("My IP address: ");    Serial.println(Ethernet.localIP());
  Serial.println ();

  pinMode(buttonPin, INPUT);
}
 
 
void loop()
{
  int buttonState = digitalRead(buttonPin);
 
  if (lastButtonState == LOW && buttonState == HIGH)
  {
    Serial.println("ButtonPressed!");
  
    if ( client.connect(server, 80) )
    {
      Serial.println("connected");
      // Make a HTTP GET request
      sprintf(s, "GET http://maker.ifttt.com/trigger/%s/with/key/%s HTTP/1.1", eventName, secretKey);
      client.println(s);
      client.print("Host: ");
      client.println(server);
      client.println("Connection: close");
      client.println();
    
      Serial.println("Sent message");
      delay( 1000 );
    }
    else
    {
      Serial.println("Error: Could not make a TCP connection");
    }
  }
  lastButtonState = buttonState;
 
  while (client.available())
  {
    char c = client.read();
    Serial.print(c);
  }
  if (!client.connected())
  {
    client.stop();
  }
}

コードをコンパイルしてボードに書き込み、LANケーブルを繋げば準備は完了です。タクトスイッチを押すとスマホに通知が飛ぶようになるはずです。

飛ぶはずです...

あれ...飛ばない...?

謎のエラー

第二のハマりポイント。私の場合ですが、何度やっても通知が来ませんでした。でもシリアルモニタを確認すると

Congratulations! You've fired the ButtonPlessed eventButtonPressed!

とか書いてあるからうまくいってるはずなんだけどなぁ...

ネットで調べてみるとこちらの記事を発見。記事によると、「Webhooks」のページから[Settings]をクリックし、飛んだ先のページの[Edit connection]をクリックしてやると改善するらしいです。この通りにやってみて再トライしてみると通知が飛ぶようになりました。

まとめ

  • Maker Channelの代わりにWebhooksを使う
  • ウェブリクエストを送ってやるとWebhooksのイベントが発火させられる
  • Webhooksがうまく動作しないときはアプレットの再接続をしてみる

これでローカルなトリガーによりネットワーク上でアクションを起こせるようになりました。次はネットワーク上のトリガーによってローカルでアクションを起こせるようにしたいですね。