ArduinoからiOSやAndroidブラウザへリアルタイムデータ通信(Server-Sent Events使用)ができました。

Arduino

こんばんは。
前回から悩んでいたのがついにできました。
今回はスゴイです!!
ネットでも殆ど情報が無いかったServer-Sent Eventsを使って、
ArduinoからiOSやAndroidブラウザへWiFi リアルタイムストリーミング通信できたんです!!
WebSocketとかPHPとかProcessingとか一切使ってません。アプリも使わず、ブラウザだけです。
ArduinoプログラムにHTMLタグとJavaScriptを載せただけです。
これ、ほんとにすごいですね。


スポンサーリンク


これは、ArduinoUNO と EthernetShiels2 (有線LAN)を使って、ルーターでWi-Fiを使用して、スマホへ飛ばしてます。
まず、動画はこちらでご覧ください。
外国の方も見てらっしゃるので、Google翻訳英語でテロップ出してます。
(著作権の関係上、音声をフリー音源に差し替えてます。申し訳ございません)

無線で音声に追従するというのがスゴイですね。
このプログラムにさらに手を加えて、オーディオレベルメーターと一緒に表示させてみました。
(著作権の関係上、音声をフリー音源に差し替えてます。申し訳ございません)

オーディオレベルメーターについては過去記事をご覧ください。
ArduinoMega直結のI2C通信のLEDドットマトリックスには少し遅れますが、結構リアルタイムに追従してくれます。
これだけ追従してくれれば、十分です。無線ですから、遠くの場所でレベル確認ができます。
これ、スゴイです!!
スマホアプリを開発したりなんか一切必要ないんです。
これ、誰かやったことのかなぁ・・・??
これは、ホームページを作成したりする HTMLタグにJavaScript の
Server-Sent Events
というものを使用します。
私が確認したブラウザ対応状況は以下の通り


Google Chrome 43.0.2357.132m (Windows Vista) OK! 恐らくWindows7や8.1もOKでしょう。
Internet Explore 9 NG
Google Chrome for Android(4.2.2) and (4.4.2) OK! 最新版ならばOKです。
Safari iOS 8.4 OK!


Internet Explore以外は意外と多くのブラウザがOKでした。
Server-sent Eventsはけっこう決まり事があり、それを調べるのにかなりの労力を費やしました。
(要するに、JavaScriptをあまり理解していないせいですが・・・)
これの一番のポイントは、HTMLファイルとデータ送信するファイルを
同じサーバーHOSTに置かなければならない
といところが重要です。
つまり、HTMLファイルだけPC上に置いてそれを開いて、Arduino からデータ送信してもまともに動きません。
この答えにたどり着くまでスゴイ時間かかりました。
ということは、Arduino自身からHTMLタグをブラウザに送信して、JavaScriptのServer-Sent Eventsを認識させて、一旦接続を切る。
それから同じArduinoからデータを送信すれば、ブラウザが認識してくれるんです。
接続を一旦切ったら、今度はブラウザはデータ待ちの状態になっているようで、結構時間をおいても認識してくれるみたいです。
このServer-Sent Eventsは終了イベント処理をしないと、一旦処理を終えて切断しても、ブラウザが勝手に再接続を試みるんです。場合によっては好都合ですが・・・。
その処理をしないと、延々とループしますんで、バッテリーかなり食います。
さて、Arduino からの送信はテキストベースで送ります。
必ず次の約束ごとがあります。
data:Text改行
改行

というdata+コロンをいれて、その後にテキストデータを送り、改行コード\nを2つ送ります。
Arduinoからはclient.println();と同じです。
改行を2つ入れることによって異なるイベントの発生が可能なんです。
改行を入れないと、続けて表示します。 data:の部分は表示されません。
このデータにイベントを付けて送信もできます。
event: msg1改行
data:Text1改行
改行
event: msg2改行
data:Text2改行
改行

と送れば、異なるイベント、msg1とmsg2というイベント発生として認識してくれます。
このイベントを検知したらJavaScriptの関数が動作するということにするわけです。
今回はウィンドウの同じ位置のテキストを上書きしてデータを入れ換えていますが、データロガーのように消さずにずらーっと表示させることもできます。
下に一番上の動画の自作ソースコードを載せておきますので、いろいろやってみてください。
何かオモシロイことができそうですよね・・・。
オモシロイものできたらご報告いただけるとありがたいです。
それでは、また・・・
※ただ、このプログラム、UNOではグローバル変数が72%、プログラムストレージが40%も消費してしまってます。
けっこう一杯いっぱいですので、client.printをうまく削るしかないです。
ちなみに、ネットワーク管理者にルーターの設定や、ファイアウォールの設定をしてもらうことを忘れずに。
このプログラムはデータを1000回送ったら自動切断します。
(動作確認はしていますが、いかなる不具合も当方で責任は負えませんのでご了承ください。
継ぎはぎのプログラムですので、誤り等あったらご連絡ください)
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include <SPI.h>
#include <Ethernet2.h> //古いイーサネットシールドの場合は Old Ethernet shield =<Ethernet.h>

byte mac[] = {
  0x9A, 0xB5, 0xFE, 0xDA, 0x56, 0x38 //Your Ethernet Shield Mac Address ご自分のイーサネットシールドのアドレス
};

IPAddress ip(192, 168, 0, 25);//Your local IP Address ご自分のルーターで割り当てたローカルIPアドレス
EthernetServer server(80);
EthernetClient client ; // クライアントオブジェクトを取得
boolean java_on = false; //ブラウザにJavaScriptを認識させたかどうかの判断を行う

void setup() {
  pinMode(4, OUTPUT);//これ重要
  pinMode(10, OUTPUT);//これ重要(Mega = pinMode(53, OUTPUT);
  digitalWrite(4, HIGH);//これ重要
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }
  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();
  Serial.print("server is at ");
  Serial.println(Ethernet.localIP());
}

void loop() {
  client = server.available();
  if(java_on==false){
    if (client) {
      Serial.println("new client");
      // an http request ends with a blank line
      boolean currentLineIsBlank = true;
      while (client.connected()) {
        if (client.available()) {
          char c = client.read();
          Serial.write(c);
          if (c == '\n' && currentLineIsBlank) {
            // send a standard http response header
            client.println("HTTP/1.1 200 OK");//まず、ブラウザにレスポンスを送る
            client.println("Content-Type: text/html");
            client.println("Connection: close");  // the connection will be closed after completion of the response
            client.println();
            client.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
            client.println("<html>");
            client.println("<head>");
            client.println("<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">");
            client.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.3, user-scalable=yes\">");
            client.println("<title>Server-Sent Events Test</title>");
            client.println("<script type=\"text/javascript\">");
            client.println("var source=new EventSource(\"192.168.0.25\");");//ここがストリーム配信の重要設定
            client.println("  source.addEventListener('msg_1',function(event){");//ここからは一般的なJavaScript
            client.println("  var ms1 = document.getElementById('msgs1');");
            client.println("  ms1.innerHTML = event.data;");
            client.println("});");
            client.println("  source.addEventListener('msg_2',function(event){");
            client.println("  var ms2 = document.getElementById('msgs2');");
            client.println("  ms2.innerHTML = event.data;");
            client.println("});");
            client.println();
            client.println("source.addEventListener('error',function(event){");//エラーイベントを受けたら終了する設定
            client.println("  source.close();");
            client.println("});");
            client.println();
            client.println("</script>");
            client.println("</head>");
            client.println("<body>");
            client.println("<FONT size=\"4\">");
            client.println("analog A0<div id=\"msgs1\">msg1</div>");
            client.println("<br>");
            client.println("analog A1<div id=\"msgs2\">msg2</div>");
            client.println("</body>");
            client.println("</html>");
            java_on=true; //ブラウザがJavaScriptとHTMLタグを全て受け取ったらtrueにする
            break;
          }
          if (c == '\n') {  //ここはサンプルプログラムと変わらない。ブラウザのGETリクエストを改行で判断している
            // you're starting a new line
            currentLineIsBlank = true;
          }else if (c != '\r') {
            // you've gotten a character on the current line
            currentLineIsBlank = false;
          }
        }
      }
      // give the web browser time to receive the data
      delay(1);
      // close the connection:
      client.stop(); //一旦クライアントと切断する。ブラウザは切断してもストリーム受信待機している。
      Serial.println("client disconnected");
    }
  }
  if(java_on == true){//ブラウザが上のJavaScriptを受け取ったかどうかの判断
    delay(3000); //3秒待ってから、Arduinoからブラウザへレスポンスする
    GET_Reponse();
    DATA_Output();
    java_on = false;
  }
}
//*****************ArduinoからServer-Sent Eventのレスポンス******************
void GET_Reponse()
{
  client = server.available();
  //ストリーム配信をブラウザが認識するためのレスポンス
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/event-stream");
  client.println("Cache-Control: no-cache");
  client.println();
}
//**********************Server-Sent Eventの EventSource データ出力**************
void DATA_Output()
{//Arduino の analogReadの値をブラウザへ1000回送ったらコネクション切断
  client = server.available();
  for(int i=0;i<1000;i++){
    client.println("event: msg_1");
    client.print("data:");client.println(analogRead(A0));
    client.println();
    client.println("event: msg_2");
    client.print("data:");client.println(analogRead(A1));
    client.println();
    delay(100);
  }
  client.println("event: msg_1");
  client.println("data:EventSource.  Close");
  client.println();
  client.println("event: msg_2");
  client.println("data:EventSource.  Close");
  client.println();
  client.println("event: close");
  // give the web browser time to receive the data
  delay(1);
  // close the connection:
  client.stop();
  Serial.println("client disconnected");
}

(関連記事)
つづき、メーターをスクロールしてみました(ServerSentEvents使用from Arduino)
つづき、Server-Sent Eventsについて(Arduinoとスマートフォン連携)
ArduinoとスマホブラウザのServer-Sent Eventsで、Canvas要素を使ってグラフ表示してみました。
ステレオレベルメーターを作ってみました。
ちょっと変なレベルメーターも作ってみました。
オーディオレベルメーターを作ってみました。
つづき、動画アップしました。Arduinoタイマー付きLED電光掲示板 試作完成 その4

[ホーム(home)]

コメント

  1. h.k より:

    読み出す時に一手間必要になりますが文字列をPROGMEMを使って格納するとメモリ消費量を抑えられるかもしれません。

    • mgo-tec mgo-tec より:

      h.kさん

      コメントありがとうございます。

      この記事はかなり前に書いたものでして、この後の記事で、ご指摘の通りprogmem 関数やclient.print(F(…)); などのFマクロを使ってメモリを節約するようにしました。

      ただ、UNOではSRAMが限られているので、最近はArduino UNOよりも安価でSRAMが膨大で、かつWi-Fi通信ができる ESP-WROOM-02というデバイスを使ってブラウザとリアルタイム双方向通信してます。
      これはArduino IDEで開発できて、Arduinoと同じライブラリが使えるのでとても便利です。
      参考までに以下のリンクをご覧ください。
      https://www.mgo-tec.com/blog-entry-wroom-websocket-messageboard-05.html
      https://www.mgo-tec.com/blog-entry-easywebsocket-beta13.html

タイトルとURLをコピーしました