おもちゃラボ

Unityで遊びを作ってます

【Arduino】ESP-WROOM-02( ESP8266 )を使ってWifiで無線通信する

マイコンを使って無線通信をする、となると一昔前までは非常に大変だったのですが、最近は便利なモジュールが安価で販売されているようです。この記事ではESP-WROOM-02と呼ばれる無線モジュールを使った無線通信の実験をしてみましょう。

f:id:nn_hokuson:20170409091121j:plain

この記事の目次は次のようになります。

無線通信のシステム構成

今回作るArduinoを使った無線通信システムの全体的な構成は次のようになります。

f:id:nn_hokuson:20170409075115p:plain:w300

ArduinoからサーバにGETでデータを送信し、サーバ側ではPHPを使ってデータを受け取ってから保存します。受け取ったデータはブラウザから確認できるようにします。

ESP-WROOM-02を使った通信に必要なものを揃える

ESP-WROOM-02

Arduino Unoでwifiを使った無線通信をするためには、無線通信用のモジュールが必要になります。

スイッチサイエンスなどから非常に安価なモジュールが販売されているため、それを利用します。今回使用したモジュールは次のESP-WROOM-02です。

ESP-WROOM-02はESP8266という無線チップを実装したモジュールになります。スイッチサイエンスからはシンプル版やフル版が販売されていますが、ここでは簡単に使えるシンプル版を使います

3端子レギュレータ

また、ESP-WROOM-02には3.3Vの電源供給が必要になります。Arduinoにも3.3Vのポートが用意されていますが、供給できる電流値が少ないため使えません。

5Vから3.3Vの電源を作るために3端子レギュレータが追加で必要になります。秋月のセットであれば、必要になるキャパシタとセットで販売されています。

f:id:nn_hokuson:20170408153422p:plain:w240
http://akizukidenshi.com/catalog/g/gI-00538/

レベルシフタ

ArduinoとESP-WROOM-02はシリアルで通信します。上にも書いたとおりESP-WROOM-02は3.3Vで動作するため、Arduinoとシリアル通信に入力するには3.3V↔5Vを変換する双方向レベルシフタが必要になります。

実はレベルシフタがなくても動作しますが、データの受信時に不安定になる傾向があります。今回はESP-WROOM-02からはデータの送信しかしないため、レベルシフタは使用していません。

wifi通信のための回路図

ArduinoとESP-WROOM-02を接続する回路図は次のようになります。
f:id:nn_hokuson:20170614231708j:plain

初期設定

ArduinoとESP-WROOM-02間の通信はソフトウエアシリアル通信を使います。ESP-WROOM-02が初期状態では115200bpsで通信するのに対して、Arduinoのソフトウエアシリアルの最高速度は9600bpsです。

このままでは安定した通信ができないため、ESP-WROOM-02の通信速度を9600bpsまで下げる必要があります。まずは次のスケッチを作成してArduinoに転送して下さい。

#include <SoftwareSerial.h>

SoftwareSerial mySerial(2, 3); // RX, TX

void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }


  Serial.println("Goodnight moon!");

  // set the data rate for the SoftwareSerial port
  mySerial.begin(115200);
  mySerial.println("Hello, world?");
}

void loop() { // run over and over
  if (mySerial.available()) {
    Serial.write(mySerial.read());
  }
  if (Serial.available()) {
    mySerial.write(Serial.read());
  }
}

転送できたら、右上のシリアルモニタボタンをクリックして、シリアルモニタを開いて下さい。

f:id:nn_hokuson:20170408155735p:plain
シリアルモニタウインドウの右下の改行コードを「CRおよびLF」、転送レートを「112500」に変更します。

f:id:nn_hokuson:20170408160013p:plain:w300

すると、シリアルモニタに次のように表示されると思います。表示されない場合はArduinoのリセットボタンを押してみて下さい。

Goodnight moon!
Hello, world?

これに続けて次のコマンドを入力して、送信ボタンを押します。

AT+UART_DEF=9600,8,1,0,0

シリアルモニタの画面上にOKが表示されれば、設定は完了です。

ESP8266のライブラリをインストール

まずはESP8266を動かすためのライブラリをインストールしましょう。
ライブラリを↓からダウンロードしてください。

www.dropbox.com

ダウンロードしたライブラリをインストールします。

Arduinoのメニューから「スケッチ」→「ライブラリをインクルード」→「.ZIP形式のライブラリをインストール」を選択し、ダウンロードしたITEADLIB_Arduino_WeeESP8266-masterをインストールします。

上記ライブラリは、本家で提供されているライブラリを修正し、ソフトウエアシリアルを使えるようにしたものです。
github.com

無線通信プログラムの作成

ではいよいよ、データを送信するためのプログラムの作成を作りましょう。次のプログラムを入力して下さい。

#include "ESP8266.h"
#include <SoftwareSerial.h>

#define SSID "Buffalo-G-XXXX"
#define PASSWORD "xxxxxxxxxxxxx"
#define HOST_NAME "192.168.xx.xx"
#define FILE_NAME "test.php"

int n = 0;

SoftwareSerial mySerial(2, 3);  //RX, TX
ESP8266 wifi(mySerial);

/**
 * 初期設定
 */
void setup(void)
{
  Serial.begin(9600);

  if (wifi.setOprToStationSoftAP()) {
    Serial.println("to station ok");
  } else {
    Serial.println("to station error");
  }

  if (wifi.joinAP(SSID, PASSWORD)) {
    Serial.println("connect success");
  } else {
    Serial.println("connect error");
  }

  if (wifi.disableMUX()) {
    Serial.println("disable mux success");
  } else {
    Serial.println("disable mux error");
  } 
}

void loop(void)
{
  n++;

  // TCPで接続
 wifi.createTCP(HOST_NAME, 80);
  
  // サーバーへ渡す情報
 char sendStr[128];
 sprintf(sendStr, "GET /%s?data=%d HTTP/1.0\r\nHost: %s\r\nUser-Agent: arduino\r\n\r\n", FILE_NAME, n, HOST_NAME);
 wifi.send((const uint8_t*)sendStr, strlen(sendStr));

  Serial.println(n);

  delay(100);
 }

SSIDとPASSWORDの欄にはお使いのルータのSSIDとパスワードを入力して下さい。HOST_NAMEの欄には、次のステップで作成するPHPファイルを置くサーバのホスト名を入力します(お使いのMacをサーバーにする場合は、自分のMacのIPアドレスを入力して下さい)

作成したプログラムをArduinoに転送して実行してみて下さい。シリアルモニタを起動して次のように表示されたらルータとの接続は成功です。転送速度を9600bpsに設定するのを忘れないようにしてくださいね〜

f:id:nn_hokuson:20170409084813j:plain:w400

サーバー側のプログラムを作っていないため、データを送信する部分は失敗します。

サーバー側のプログラムを作る

最後にArduinoから送信されたデータをサーバで受け取るプログラムを作りましょう。

f:id:nn_hokuson:20170409091847p:plain:w300

サーバー側のプログラムはPHPで作成しています。次のプログラムを入力して「test.php」という名前で保存して下さい。

<?php
$data = $_GET['data'];
if($data=='a'){
    $fp = fopen("data.txt", "r");
    $contents = fread($fp, filesize($filename));
    print($contents);
    fclose($fp);
}
else
{
    $fp = fopen("data.txt", 'w');
    fwrite($fp,  $data);
    fclose($fp);
}
?>

このプログラムはGETで送られてきたデータが

  • 「a」だったら保存しているデータをブラウザ上に表示し
  • 「a」以外だったらそのデータをファイルに書き込む

という、超適当なものです(^^;;)

今回は自宅のMacをサーバーとして使うので、上記ファイルを/Library/WebServer/Documents/フォルダ(Macの場合)に保存します。Macをお持ちではない場合は、お使いのサーバにアップロードしておいて下さい。

Macをサーバーとして動かすためには次の手順が必要になります。

  1. httpd.confの修正
  2. Apacheの起動
  3. ファイルの配置

この手順は次のサイトに詳しくまとめられていましたので、参考にして下さい。
qiita.com

動かしてみる

これですべての要素が整いました。Arduinoのリセットボタンを押してプログラムを再起動してシリアルモニタを確認してみましょう。次のように表示されていれば、ESP-WROOM-02からのデータ送信は成功です。

f:id:nn_hokuson:20170409085329j:plain:w500

次にブラウザからサーバのプログラムにアクセスし、ちゃんとデータが送られてきているかを見てみましょう。

f:id:nn_hokuson:20170409092152p:plain:w300

ブラウザのアドレスバーには次のように入力してアクセスして下さい。

192.168.xx.xx/test.php?data=a

次のように表示されていれば受信も成功です!ESP-WROOM-02から正常にデータが送信されていれば、ページをリロードするとどんどん数字が増加していくはずです。

f:id:nn_hokuson:20170409082927p:plain:w300

まとめと参考

ESP-WROOM-02とArduinoを使った無線通信の実験をしました。無線通信と聞くと難しく感じられるかもしれませんが、安価に始められるので是非挑戦してみて下さいね!

以下は参考にさせていただいた記事です。
https://ics.media/entry/10457/2ics.media
www.mgo-tec.com
www.qoosky.io

[asin:4798046604:detail]
[asin:4873117895:detail]

【Unity】Animatorでステートが遷移するタイミングを検知する

Animatorを使っていると、あるステートから別のステートへ遷移したタイミングを検知して、アクションを起こしたいことがあります。

例えばジャンプアニメーションが終了して着地したタイミングで効果音を鳴らしたいとか、カメラを振動させたい、といった場合です。

このような場合はStateMachineBehaviourというコールバック用のスクリプトを使うと便利です。

f:id:nn_hokuson:20170407193927p:plain

StateMachineBehaviourの使い方

StateMachineBehaviourはAnimatorにアタッチして使う専用のスクリプトです。ここでは2Dユニティちゃんのサンプルを使って、ジャンプ終了のタイミングを検知してみます。まずはAnimatorを開くと次のような構成になっています。

f:id:nn_hokuson:20170407194657p:plain

Animatorにスクリプトをアタッチするため、ウインドウ右側のインスペクタに表示されている「Add Behaviour」をクリックして、スクリプトをアタッチしてください。

f:id:nn_hokuson:20170407194625p:plain

作成したスクリプトをプロジェクトビューから開くと、次のコールバック関数がコメントアウトされています。

  • OnStateEnter
  • OnStateExit
  • OnStateMachineEnter
  • OnStateMachineExit
  • OnStateMove
  • OnStateUpdate
  • OnStateIK

よく使うのは、OnStateEnterとOnStateExitです。これらはそれぞれ、ステートに入ったときとステートから出たときに呼び出されるコールバック関数です。

OnStateMachineEnter/Exitはそれぞれスクリプトをアタッチしたアニメーションレイヤに入ったときと出たときに呼び出されます。

StateMachineBehaviourから別のメソッドを呼び出す

ジャンプが終了したタイミングでカメラを振動させたいので、ステートが終了したときに呼び出されるOnStateExitメソッドを使います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StateController : StateMachineBehaviour {
    // OnStateExit is called before OnStateExit is called on any state inside this state machine
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        if (stateInfo.IsName ("JumpDown"))
        {
            Camera.main.GetComponent<CameraShaker> ().Shake ();
        }
    }
}

ここでは、ジャンプが終了したタイミング(JumpDownステートを抜けるタイミング)を検知するため、AnimatorStateInfoのIsNameメソッドを使って、現在のステートがJumpDownステートかどうかを調べています。

JumpDownであれば、カメラオブジェクトにアタッチしたCameraShakerスクリプトのShakeメソッドを呼び出しています。

カメラを振動させるスクリプトをアタッチする

ユニティちゃんがジャンプし終わったときに呼び出すスクリプトを作ります。
カメラを振動させるスクリプトは次のようになります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraShaker : MonoBehaviour {
    IEnumerator _Shake() {
        for (int i = 0; i <= 360 * 1; i += 60) {
            float y = 0.2f*Mathf.Sin (i * Mathf.Deg2Rad);
            transform.position = new Vector3 (0, y, -10);
            yield return null;
        }
    }
    public void Shake() {
        StartCoroutine (_Shake ());
    }
}

これをヒエラルキービューのMainCameraにアタッチしておきましょう。

f:id:nn_hokuson:20170407194844j:plain

実行結果

実行結果は次のようになります。ジャンプの終了のタイミングでちゃんと画面が振動していますね。

f:id:nn_hokuson:20170407194853g:plain
© Unity Technologies Japan/UCL

【Unity】プログレスバーを作るときにハマりがちな3つの誤り

Unityでプログレスバー(progressbar)を作るときは、uGUIのImageを使うことが多いと思います。Imageの設定をFillに設定することで簡単にプログレスバーのようなものを作ることができます。

f:id:nn_hokuson:20170405192003p:plain

スクリプトからプログレスバーを更新する場合は、次のようにImageオブジェクトのfillAmountの項目を0.0〜1.0の間で設定することで表示の割合を変えることができます。

GetComponent<Image>().fillAmount = 0.5;

やることはこれだけなのですが、重い処理に合わせてプログレスバーを進めようとするとなかなかうまく行きません。次の3つが失敗例。最後に成功例を載せています。

誤り実装① コルーチンを使う

IEnumerator Progress()
{
    for(int i = 0; i <= 100; i++){
        GetComponent<Image>().fillAmount = i / 100.0f;
        yield return null;
    }
}

void HeavyProcess()
{
    StartCoroutine(Progress());
    
    //以下重たい処理
    // ・・・・
}

重たい処理をはじめる前にコルーチンを生成し、そちらでなんちゃってアニメーションしようという案です。

Unityのコルーチンはスレッドっぽくてスレッドではない(並列動作しない)ため、この実装は正常に動きません。重たい処理に引きずられて、Progressコルーチンは重たい処理が終わったあとにようやく実行されます。

誤り実装② 処理の間に挟み込む

void HeavyProcess()
{
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.2f;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.4f;
    //超重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.9f;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 1.0f;
}

コルーチンがだめなら、直接重たい処理の中にプログレスバーの更新プログラムを挟み込めばいいじゃん案です。

なかなか良さそうですが、残念ながらこれも正常には動作せず、いきなりプログレスバーの進捗が100%になってしまいます。

これは、fillAmountに値を代入したあとも重たい処理が続くため、uGUIの画面更新が行われないのが原因です。当たり前といえば当たり前か・・・・

誤り実装③ Threadを使う

private Thread _thread;
IEnumerator Progress()
{
    for(int i = 0; i <= 100; i++){
        GetComponent<Image>().fillAmount = i / 100.0f;
        Thread.Sleep(10);
    }
}

void HeavyProcess()
{
    _thread = new Thread(Progress());
    _thread.Start();
    
    //以下重たい処理
    // ・・・・
}

コルーチンがだめなら非同期スレッドだ、案です。そもそもUnityの設計思想がシングルスレッドなので、Threadを使う時点でイケてないのですが・・・Threadの中ではuGUIの更新ができずにエラーが出ます。

ちゃんとプログレスバーが伸びる実装

IEnumerator HeavyProcess()
{
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.2f;
    yield return null;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.4f;
    yield return null;
    //超重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.9f;
    yield return null;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 1.0f;
}

void Hoge()
{
    StartCoroutine(HeavyProcess());
}

さて、いよいよ正しく動く実装です。結局何をしたかというと、コルーチンと挟み込みの折衷案です。重たい関数自体をコルーチンにして、プログレスバーをすすめるたびに、一旦処理をメインスレッドに返します。これにより描画処理が実行され、プログレスバーも進む、という仕組みです。

これにて一件落着!?

f:id:nn_hokuson:20170405192018g:plain