WebSocket でスマホから Arduino化 WROOM のLEDを調光してみる

記事公開日:2015年11月5日
最終修正日:2018年9月26日

さて、今回はいよいよ Arduino化 した ESP-WROOM-02 ( ESP8266 )にLEDを接続して、スマホブラウザからWebSocket通信でLEDの Wi-Fi 調光制御(明るさ調整)してみましょう。
さらにこれは双方向ストリーミング通信なので、WROOMから生成したデータをスマホブラウザに同時表示してみましょう。

前々回の記事ではWebSocketハンドシェイク(コネクション確立)方法の概要を説明しました。
前回の記事ではWebSocketデータの送受信方法を説明しました。

そちらの方も参照していただきながら作っていきたいと思います。

以前にアップした記事の時の動画よりはかなりプログラムを改善して、意図しない切断が殆ど無く動くようになりました。
まず、こちらの動画をご覧ください。

どうですか・・・、殆どストレス無く双方向ストリーミング通信してくれていると思いませんか?
これが Wi-Fi 通信しているとなると、なお感慨深いものがあります。「完全無線通信を我が手中についに収めた!」 ていう感じです。

前半は Androidスマホで、ブラウザは Google Chrome です。
後半は iPad mini で、ブラウザは Safari です。
iPadのスライダーは操作しにくいですね。何か別の方法があるんでしょうか?

赤い一桁の数値がArduino化したESP-WROOM-02 ( ESP8266 )から0.3秒毎にカウントアップするデータを受信した数値です。
その下のテキストボックスの数値変化はブラウザ上で動作している JavaScript のデータ表示ですので、これはWi-Fi通信とは関係ありません。

これは以前の記事で紹介したストリーミングのServer-Sent Events に負けない表示能力だと思います。
Server-Sent Events ではストリーミングは受信のみでしたが、WebSocketはスマホからも同時にデータをリアルタイムに送信できるんです。
まるで、アプリを使っているかのようですが、ブラウザだけというところがスゴイですよね。
しかも、ブラウザだけでアプリ不要ですから端末は最新式の物であれば何でもいいんです。

スマホブラウザのスライダーを動かすとWROOMのGPIOピンに接続したLEDの明るさがリアルタイムに変化してくれています。
けっこう素早い動作にも追従してくれています。
やはり、完全な全二重通信というわけではなく、WROOMで送信している間は受信できません。時間差通信ですので、疑似双方向といったところでしょうか。
送受信を同時に行うと、送信、受信どちらかの通信速度に影響が出ますね。
でも、これくらにならそれほど気にならないレベルです。

では、いよいよこれを作っていきましょう。

スポンサーリンク

1.準備するもの

No.品名個数参考購入先
1ESP-WROOM-021Amazon,マイクロテクニカ販売のものなど。
秋月さんや楽天でも売ってます。
2LED (5V用)1
3抵抗
(例えば100Ωなど。
LEDに流れる電流を20mA以下に抑えるため)
1
3超小型USBシリアル変換モジュール
(3.3V対応)
1秋月電子通商
4三端子レギュレーター
TA48M033F
(秋月さんで購入すると電界コンデンサーとセラミックコンデンサー込でした)
1秋月電子通商
4電解コンデンサー
47uF 35V
1
5積層セラミックコンデンサー
0.1uF
2
6ブレッドボード1
7ジャンパーワイヤー等
8パソコン、USBケーブル,等
9最新のiOSやAndroidスマホ
および、SafariかGoogleChrome
10WROOMがマイクロテクニカ販売のものならば、
2mmピンヘッダ
2mmジャンパーピン
2.54mmピンヘッダ
1式
11リセット用超小型タクトスイッチ
2mmピンヘッダに合うもの
1

ESP-WROOM-02 は現在はamazonのマイクロテクニカ販売のものを使用してます。
(※2015年当初とはタイプが異なっています)
LEDは手持ちの物を使用しました。5V用のものならば、何でもいいかと思います。

2.接続とハンダ付けをする

ESP-WROOM-02 本体のヘッダピンやリセットスイッチの接続は こちら の記事を参考にしてください。2mmピンヘッダとジャンパーピン、超小型タクトスイッチをハンダ付けしてます。
マイクロテクニカ販売のWROOMを購入すると、取扱説明書がダウンロードできますのでそれも参照してみてください。

3.3V対応USBシリアル変換モジュールは秋月電子通商で販売しているものを使用しましたが、3.3V対応のものであれば何でもよいかと思います。

その他の接続は下図のようにします。
WebSocket-wroom03-02
LEDはWROOMの GPIO 13番 に接続します。
LEDに接続してある抵抗は、ESP-WROOM-02保護のために抵抗を接続しました。
ESP-WROOM-02 ( ESP8266 ) のGPIO端子に流せる電流は最大12mAまでです。
それより一瞬でもオーバーしてしまうと、ESP-WROOM-02 ( ESP8266 )が即壊れる場合がありますので、ご注意ください。

また、LEDの色によっては電流値が変わりますので、程よい明るさになるように抵抗を選べばよいと思います。
今回は20Ωを使いました。

3.Arduino IDE を設定しておく

ESP-WROOM-02 ( ESP8266 ) をArduino化するためのIDEの設定は以下の記事を参照して、事前に済ませておいてください。

Arduino IDE に Staging(Stable)版ESP8266 ボードをインストールする方法

因みに、過去記事でも書いておりますが、Arduino.orgサイトのIDE(1.7.2等)では ESP8266ボードをインストールできないのでご注意ください。Arduino.ccのページ1.6.12を使用します。
(この記事を書いた時点からかなり年月が経過しています。おそらく最新バージョンでも動作すると思われます。)

4.対応しているブラウザについて

メジャーなブラウザならば、最新バージョンであれば殆どのものが対応しています。
Internet Explorer は10からしか対応していないようです。
私が実験したバージョンは以下の通りです。

Android 4.2.2以降 最新版Google Chrome
iOS safari

Android4.2.2は3年くらい前のスマホを使ってみました。
CPUやメモリは最近のスマホに比べればかなり非力ですが、とりあえず問題なく動作しました。
最新式のスマホでAndroidならば、殆ど意図しないコネクション切断がありませんでしたが、なぜかiPadでは最新式でもコネクション切断が起きる時があります。
頻繁にではないのですが、これの原因はまだ突き止めておりません。

5.スマホとルーターを予めWi-Fi接続しておき、WROOM をアクセスできるようにしておく

ご自分のルーターの電源を立ち上げ、事前にスマートフォンとWi-Fi接続しておきます。
その時に SSID や パスワード、アクセス制限などの設定を確認しておいてください。
そして、ESP-WROOM-02 のローカルIPアドレスを固定にしておくと良いと思います。
MACアドレスが分からない場合などはATコマンド通信を行っていた過去の記事を参照してください。
一回設定が済むと、私の場合は電源切っても設定不要でした。

6.Hash.h ライブラリの確認

まず、Hash.h ライブラリが存在するかどうか確認してください。
確認方法は下図を参照してください。
WebSocket-wroom03-03

ライブラリが存在しない場合はESP8266ボードマネージャーがインストールされていない場合がありますので、再度インストールしてください。

7.Arduino IDE を使って ESP-WROOM-02 にスケッチを書き込む

では、
前々回の記事 WebSocketハンドシェイク確立方法
前回の記事 WebSocketデータ送受信方法
を参照していただきながら、それを踏まえた私なりのスケッチは以下の通りです。
私はプログラマーではないので、スマートなプログラムではないと思います。無駄も多いと思います。
エラー処理も殆どできておりませんのでご容赦ください。
かなり長いです。

※プログラムを修正しました。
スマホからのスライダー操作で連続したデータがWROOMへ送られてくるため、オーバーフローを起こし、通信が切断されてしまうことがありました。
s_rateという変数にミリセコンド単位で数値を変えると、データの間引き送信するようにしました。0~50msくらいの間で数値を調整するといいと思います。
ただ、追従性は悪くなります。(2015/12/3)
#include <Arduino.h>
#include <Hash.h>
#include <ESP8266WiFi.h>
 
//ご自分のルーターのSSIDを入力してください
const char* ssid = "XXXX";
//ご自分のルーターのパスワード
const char* password = "XXXX";
//ご自分のルーターのローカルIPアドレスの数値部分のみ
const char* LocalIPaddress = "XXX.XXX.XXX.XXX";
 
boolean Ini_html_on = false;//ブラウザからの初回HTTPレスポンス完了したかどうかのフラグ
boolean WS_on = false;//WebSocket設定が済んだかどうかのフラグ
WiFiServer server(80);
 
char Android_or_iPad; //スマホが Android か iPad かを判定するフラグ
 
//LED点灯用ピンアサイン GPIO 13
#define ledPin 13

//通信トラフィックをオーバーフローを起こさせないようにする変数。
//ミリセコンド単位でスマホからのスライダー値送信を間引く
byte s_rate = 10;
 
void setup() {
  Serial.begin(115200);
  // Connect to WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
   
  WiFi.begin(ssid, password);
   
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
   
  // Start the server
  server.begin();
  Serial.println("Server started");
 
  // Print the IP address
  Serial.println(WiFi.localIP());
 
  pinMode(ledPin, OUTPUT); 
}
 
//************メインループ********************************
void loop() {
  if(Ini_html_on == false){
    Ini_HTTP_Response();
  }
  if(Ini_html_on == true && WS_on == false){
    WS_HTTP_Responce();
  }
  delay(1);//これは重要かも。これがないと動作かも。
}
 
//*****初回ブラウザからのGET要求によるJavaScript吐き出しHTTPレスポンス*******
void Ini_HTTP_Response()
{
  WiFiClient client = server.available();//クライアント生成は各関数内でしか実行できないので注意
  String req;
   
  while(client){
    req = client.readStringUntil('\n');
    Serial.println(req);
    if (req.indexOf("GET / HTTP") != -1){//ブラウザからリクエストを受信したらこの文字列を検知する
      Serial.println("-----from Browser FirstTime HTTP Request---------");
      Serial.println(req);
      //ブラウザからのリクエストで空行(\r\nが先頭になる)まで読み込む
      while(req.indexOf("\r") != 0){
        req = client.readStringUntil('\n');//\nまで読み込むが\n自身は文字列に含まれず、捨てられる
        //ここでブラウザがChromeかSafariかをリクエスト文字列から判定
        if(req.indexOf("Android") != -1){
          Android_or_iPad = 'A';
        }else if(req.indexOf("iPad") != -1){
          Android_or_iPad = 'i';
        }
        Serial.println(req);
      }
      req = "";
      delay(10);//10ms待ってレスポンスをブラウザに送信
 
      //メモリ節約のため、Fマクロで文字列を囲う
      //普通のHTTPレスポンスヘッダ
      client.print(F("HTTP/1.1 200 OK\r\n"));
      client.print(F("Content-Type:text/html\r\n"));
      client.print(F("Connection:close\r\n\r\n"));//1行空行が必要
      //ここからブラウザ表示のためのHTML JavaScript吐き出し
      client.println(F("<!DOCTYPE html>"));
      client.println(F("<html><head>"));
      client.println(F("<meta charset='utf-8'>"));
      client.println(F("<meta name='viewport' content='initial-scale=1.1'>"));
      client.println(F("<title>WebSocket Test</title>"));
      client.println(F("<script language='javascript' type='text/javascript'>"));
      client.print(F("var wsUri = 'ws://"));
      client.print(LocalIPaddress); //ローカルIPアドレス
      client.println(F("';"));
      client.println(F("var output;"));
      client.println(F("var websocket = null;"));
      client.println(F("var ms;"));
      client.println(F("function init()"));
      client.println(F("{ms = new Date();"));
      client.println(F("output = document.getElementById('output');"));
      client.println(F("testWebSocket();}"));
      client.println(F("function testWebSocket()"));
      client.println(F("{"));      
      client.println(F("if(websocket == null){"));
      client.println(F("websocket = new WebSocket(wsUri);"));//WebSocketオブジェクト生成
      client.println(F("websocket.onopen = function(evt) { onOpen(evt) };"));
      client.println(F("websocket.onclose = function(evt) { onClose(evt) };"));
      client.println(F("websocket.onmessage = function(evt) { onMessage(evt) };"));
      client.println(F("websocket.onerror = function(evt) { onError(evt) };}"));
      client.println(F("}"));
      client.println(F("function onOpen(evt)"));
      client.println(F("{writeToScreen('CONNECTED'); doSend('WebSocket rocks');}"));
      client.println(F("function onClose(evt)"));
      client.println(F("{"));
      client.println(F("writeToScreen('WS.Close.DisConnected');"));
      client.println(F("websocket.close();"));
      client.println(F("}"));
      client.println(F("function onMessage(evt)"));
      client.println(F("{var ms1 = document.getElementById('WROOM_DATA');"));
      client.println(F("ms1.innerHTML = evt.data;}"));
      client.println(F("function onError(evt)"));
      client.println(F("{writeToScreen(\"<span style='color: red;'>ERROR:</span> \" + evt.data);}"));
      client.println(F("function doSend(data)"));
      client.println(F("{var mms = new Date();"));
      client.println(F("if(mms-ms>"));
      client.print(s_rate);
      client.println(F("){websocket.send(data);"));
      client.println(F("ms = new Date();"));
      client.println(F("}}"));
      client.println(F("function WS_close()"));
      client.println(F("{websocket.close();}"));
      client.println(F("function writeToScreen(message)"));
      client.println(F("{var msg = document.getElementById('msg');"));
      client.println(F("msg.innerHTML = message;}"));
      client.println(F("window.onload = function(){"));
      client.println(F("setTimeout('init()', 3000);}"));
      client.println(F("</script></head>"));
      client.println(F("<body>"));
      client.println(F("<h2 style='color:#5555FF'><center>ESP-WROOM-02(ESP8266)<br>"));
      client.println(F("WebSocket Test</center></h2>"));
      client.println(F("from WROOM DATA = "));
      client.println(F("<font size=4>"));
      client.println(F("<span id='WROOM_DATA' style='font-size:45px; color:#FF0000;'></span>"));//改行しない場合は<span>を使う
      client.println(F("<br>JS-innerHTML="));
      client.println(F("<input type='number' name='v_box' id='v_box' style='width:30px'>"));
      client.println(F("<br><br><center>LED dimming  "));
      client.println(F("<input type='range' name='slider' ontouchmove=\"doSend(this.value); document.getElementById('v_box').value=this.value;\">"));
      client.println(F("</center><br><br>"));
      client.println(F("<div id='msg' style='font-size:25px; color:#FF0000;'></div><br>"));
      client.println(F("<input type='button' id='WS_close' value='WS.CLOSE' style='width:150px; height:40px; font-size:17px;' onclick='WS_close()'>"));
      client.println(F("<br>"));
      client.println(F("</body></html>\r\n"));
 
      delay(1);//これが重要!これが無いと切断できないかもしれない。
      client.stop();//一旦ブラウザとコネクション切断する。
      delay(1);
      Serial.println("\nGET HTTP client stop--------------------");
      req = "";
       
      //スマホがiPadならばループを抜け出す
      if(Android_or_iPad == 'i'){
        Ini_html_on = true;  //初回HTTPレスポンス終わったらtrueにする。
        break;
      }
       
    }else if(req.indexOf("favicon") != -1){
      //ChromeはGetリクエストの直ぐ後のfaviconを投げかけてくるところの対処
      Serial.println();
      Serial.println("******GET favicon Request************");
      Serial.print(req);
      while(client.available()){
        //ブラウザからデータが送られている間読み込む
        Serial.write(client.read());
      }
      delay(1);
      client.stop();  //GET/faviconでも一旦ブラウザとコネクション切断する必要あり。
      delay(1);
      Serial.println();
      Serial.println("Client Stop--------------");
      Ini_html_on = true; //HTTPレスポンス終わったらtrueにする。
      break;
    }
  }
}
 
//************HTTPレスポンスとデータ送受信関数**************************
void WS_HTTP_Responce()
{
  WiFiClient client = server.available();//クライアント生成は各関数内でしか実行できないので注意
  String req;
  String hash_req_key;
   
  while(client){
    req = client.readStringUntil('\n');
    Serial.println(req);
    if (req.indexOf("websocket") != -1){//ブラウザからリクエストを受信したらこの文字列を検知する
      Serial.println("-----from Browser HTTP WebSocket Request---------");
      Serial.println(req);
      //ブラウザからのリクエストで空行(\r\nが先頭になる)まで読み込む
      while(req.indexOf("\r") != 0){
        req = client.readStringUntil('\n');//\nまで読み込むが\n自身は文字列に含まれず、捨てられる
        Serial.println(req);
        if(req.indexOf("Sec-WebSocket-Key")>=0){
          hash_req_key = req.substring(req.indexOf(':')+2,req.indexOf('\r'));
          Serial.println();
          Serial.print("hash_req_key =");
          Serial.println(hash_req_key);
        }        
      }
 
      delay(10);
      req ="";
 
      char h_resp_key[28];
      //ハッシュ値、BASE64エンコード関数
      Hash_Key(hash_req_key, h_resp_key);
       
      Serial.print("h_resp_key = ");
      Serial.println(h_resp_key);
      String str;
      //-------ここからHTTPレスポンスのHTMLとJavaScriptコード
      str = "HTTP/1.1 101 Switching Protocols\r\n";
      str += "Upgrade: websocket\r\n";
      str += "Connection: Upgrade\r\n";
      str += "Sec-WebSocket-Accept: ";
      for(byte i=0; i<28; i++){
        str += h_resp_key[i];
      }
      //"Sec-WebSocket-Protocol: chat\r\n";これは不要。これを入れるとコネクションできない。
      str += "\r\n\r\n";//空行は必須
       
      Serial.println("-----HTTP Respons start-------");
      Serial.println(str);
      client.print(str);
      str = "";
 
      WS_on = true;//WebSocket 設定終了フラグ
 
    }else if(req.indexOf("favicon") != -1){
      //Chromeでfaviconを2回連続で投げてきた時の対処
      Serial.println();
      Serial.println("******GET favicon Request************");
      Serial.print(req);
      while(client.available()){
        //ブラウザからデータが送られている間読み込む
        Serial.write(client.read());
      }
      delay(1);
      client.stop();  //GET/faviconでも一旦ブラウザとコネクション切断する必要あり。
      delay(1);
      Serial.println();
      Serial.println("Client Stop--------------");
      Ini_html_on = true; //HTTPレスポンス終わったらtrueにする。
      break;
    }
    delay(10);
     
    //ここからWebSocetデータ送受信。    
    if(WS_on == true){
      byte b=0;
      byte data_len;
      byte mask[4];
      byte data_b;
      byte i;
      byte cnt = 0;
 
      long PingLastTime = millis();
      long PongLastTime = millis();
      long CountTestTime = millis();
   
      while(client){
        //ブラウザがping受信して1秒後までにPongを受信しない場合、コネクション切断する。
        if(millis() - PongLastTime > 4000) break;
        //データ受信が無い時に3sec毎にping送信-----------------------
        if(millis()-PingLastTime > 3000){
          client.write(B10001001);
          client.write(4);
          client.print("Ping");
          //ブラウザにPing送信すると送信した文字そのものが返って来る。
          Serial.println("Ping Send-----------");
          PingLastTime = millis();
        }
        //WROOMのカウンター数値を300ms毎にブラウザに送信
        //あまり秒数が短いとエラーになりクローズするので注意
        if(millis()-CountTestTime > 300){
          client.write(B10000001);//データ送信ヘッダ
          client.write(1);//送信文字数
          if(cnt>9){
            cnt = 0;
          }
          client.print(cnt);
          cnt++;
          CountTestTime = millis();
        }
         
        if(client.available()){
          b = client.read();
          if(b == B10000001 || b == B10001010){
            //B10001010はPongデータ受信
            switch (b){
              case B10000001:
                //ブラウザからデータ受信している時はPing送信しないようにする。
                PingLastTime = millis();
                PongLastTime = millis();
                break;
              case B10001010:
                PongLastTime = millis();
                Serial.println("Pong Receive**********");
                break;
            }
 
            b = client.read();
            //マスクビットを削除
            data_len = b - B10000000;
             
            //マスクキーを読み込む
            for(i=0; i<4; i++){
              mask[i] = client.read();
            }
             
            byte m_data[data_len];
            char data_c[data_len];
             
            Serial.print("Receive Data = ");            
            for(i = 0; i<data_len; i++){
              //マスクされたデータを読み込む
              m_data[i] = client.read();
              //マスクキーとマスクデータをXOR演算すると実テキストデータが得られる
              data_c[i] = mask[i%4]^m_data[i];
              Serial.print(data_c[i]);
            }
            Serial.println();   
                               
            //テキストデータを数値に変換
            switch (data_len){
              case 1:
                data_b = data_c[0]-0x30;//Char型を数値に変換
                break;
              case 2:
                data_b = ((data_c[0]-0x30)*10) + (data_c[1]-0x30);
                break;
              case 3:
                data_b = ((data_c[0]-0x30)*100) + ((data_c[1]-0x30)*10) + (data_c[2]-0x30);
                break;
            }
     
            LED_PWM(data_b);  //LED点灯関数
 
          }else if(b == B10001000){
            delay(1);
            client.write(B10001000);
            delay(1);
            Serial.println("Close Send------------");
            Serial.println(b,BIN);
             
            break;
          }
        }
      }
      delay(1);
      client.stop();
      delay(1);
      Serial.println();
      Serial.println("Client.STOP-----------------");
      WS_on = false;
      Ini_html_on = false;
      break;
    }
  }
}
//************ハッシュ値、BASE64エンコード関数**************************
void Hash_Key(String h_req_key, char* h_resp_key)
{
  char Base64[65] = { 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
                      'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
                      'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
                      'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/',
                      '='
                    };
  byte hash_six[27];
  byte dummy_h1, dummy_h2;
  byte bb;
  byte i, j;
  i=0;
  j=0;
   
  String GUID_str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
  String merge_str;
 
  merge_str = h_req_key + GUID_str;
  Serial.println();
  Serial.print("merge_str =");
  Serial.println(merge_str);
  Serial.print("SHA1:");
  Serial.println(sha1(merge_str));
 
  byte hash[20];
  sha1(merge_str, &hash[0]);
 
  Serial.print("SHA1:");
  for(uint16_t i = 0; i < 20; i++) {
      Serial.printf("%02x", hash[i]);
      Serial.print("-");
  }
  Serial.println();
  Serial.print("SHA1:");
  for(uint16_t i = 0; i < 20; i++) {
      Serial.print(hash[i],BIN);
      Serial.print("-");
  }
  Serial.println();
 
  for( i = 0; i < 20; i++) {
    hash_six[j] = hash[i]>>2;   
    hash_six[j+1] = hash[i+1] >> 4;
    bitWrite(hash_six[j+1], 4, bitRead(hash[i],0));
    bitWrite(hash_six[j+1], 5, bitRead(hash[i],1));
     
    if(j+2 < 26){
      hash_six[j+2] = hash[i+2] >> 6;
      bitWrite(hash_six[j+2], 2, bitRead(hash[i+1],0));
      bitWrite(hash_six[j+2], 3, bitRead(hash[i+1],1));
      bitWrite(hash_six[j+2], 4, bitRead(hash[i+1],2));
      bitWrite(hash_six[j+2], 5, bitRead(hash[i+1],3));
    }else if(j+2 == 26){
      dummy_h1 = 0;
      dummy_h2 = 0;
      dummy_h2 = hash[i+1] << 4;
      dummy_h2 = dummy_h2 >>2;
      hash_six[j+2] = dummy_h1 | dummy_h2;
    }
     
    if( j+3 < 27 ){
      hash_six[j+3] = hash[i+2];
      bitWrite(hash_six[j+3], 6, 0);
      bitWrite(hash_six[j+3], 7, 0);
    }else if(j+3 == 27){
      hash_six[j+3] = '=';
    }
     
    h_resp_key[j] = Base64[hash_six[j]];
    h_resp_key[j+1] = Base64[hash_six[j+1]];
    h_resp_key[j+2] = Base64[hash_six[j+2]];
     
    if(j+3==27){
      h_resp_key[j+3] = Base64[64];
      break;
    }else{
      h_resp_key[j+3] = Base64[hash_six[j+3]];
    }
     
    i = i + 2;
    j = j + 4;
  }
 
  Serial.print("hash_six = ");
  for(i=0; i<28; i++){
    Serial.print(hash_six[i],BIN);
    Serial.print('_');
  }
  Serial.println();
}
//************LED_PWM出力関数**************************
void LED_PWM(byte data_b)
{
  //analogWriteは 0-255 の値。とりあえずスライダー値の2倍にした。
  analogWrite(ledPin, data_b*2.5);
}

●6~8行目:ご自分のルーターのSSID、パスワードに書き換えてください。
●10行目:ルーターで設定した ESP-WROOM-02 のローカルIPアドレスに書き換えてください。
●23行目:スマホのスライダーを動かすと、連続したデータが怒涛のようにWROOMへ送られてくるので、オーバーフローを起こして通信切断されるようです。よって、スマホ側のJavaScriptで間引き送信するための変数です。0~50ms の間で数値を設定すると良いと思います。ただし、追従性は悪くなります。
●68~84行目: ブラウザのURL入力欄に ESP-WROOM-02 のローカルIPアドレスを入力したらブラウザから送信されるGETリクエストを受信するプログラムです。78行目~82行目でブラウザがAndroidかiPadか判定してます。過去の記事では Accept-Language: という行まで読み込んでいましたが、iPadではこれが最後の行ではないのです。最後の行まできっちり読み取らないと次のデータ送受信までデータが残って被ってしまうようです。うまく捨ててくれないような気がします。
そこで、ネットにあるセオリー通りに \r\n (¥r¥n)が行の先頭に現れる、つまり空行が現れるまで受信するようにしました。そうすると意図しない切断が少なくなったような気がします。
●90~160行目:ブラウザにHTTPレスポンスヘッダを付加してHTMLページを送信します。ここで文字列を
F(xxxxx)
でくくるとメモリー節約になります。
ここのHTMLタグや JavaScript については、前回の記事を参照してください。
●131~137行目:ここで、スマホ側のJavaScriptでスライダー値をスマホへ送信する関数です。ここでs_rateで指定した時間の間引き送信を行います。ゼロだと間引きしません。
●163行目:レスポンスを送った後にブラウザとコネクションを切断します。これ重要。
●169~172行目:Safari はfaviconを投げかけて来ないのでフラグをtrueにしたらループを抜けます。
●174~190行目:Google Chrome の場合は切断後すぐに GET /favicon を送信してきますのでそのメッセージをすべて受信し切ります。それが終わったらフラグをtrueにしてコネクション切断します。
●197行目:ここが要注意行です。WiFiClientは関数をまたいで定義できないのです。WebSocketハンドシェイクのみの関数、データ送受信のみの関数と分けてしまうとWebSocket-keyが再発行になってしまうようで、データ送受信ができないんです。つまり、一関数内でハンドシェイクとデータ送受信を完結させなければならないようです。不便ですね~~・・・。他に良い方法があれば教えてください。(これは実はグローバル変数領域でClientを宣言すれば良かったことが分かりました。2015/12/3)
●201~217行目:ブラウザで表示されたページからJavaScriptによってWebSocket接続リクエストが送られてきたら受信します。そしてWebSocket-keyを抽出します。これも \r\n が行の先頭に来たら受信を終了します。しかし、ここではコネクション切断しません。これが重要です。切断してしまうとWebSocket-Keyが変化してしまってハンドシェイク確立できません。
●224行目:380行目からの関数を実行して生成したキーを得ます。
●230~245行目:WebSocketレスポンスヘッダを生成したキーと共にブラウザへ送信します。これでハンドシェイク完了です。
●247~263行目:稀にGoogle Chrome で2回連続して GET /faviconリクエストが来るので、その対処です。
●281~290行目:3秒毎にブラウザへping送信します。ブラウザからpongが返って来ない場合は強制コネクション切断します。
●293~302行目:300ms毎にブラウザへデータ送信します。データの前にB10000001のヘッダを付け、文字数を決めてデータを送信します。
●304~339行目:ブラウザから送られてきたデータはマスク処理されているので、マスクキーを読み込み、それとXOR演算して実データを抽出します。
●343~353行目:抽出したデータをASCII文字に変換
●355行目:抽出したデータからLEDをPWM制御して明るさ調整します。
●357~365行目:ブラウザからクローズコマンドB10001000を受けたら、ブラウザへ同じコマンドを送信してクローズハンドシェイクを確立させます。
●380~470行目:ハンドシェイク確立のためのハッシュキーを生成するプログラム。397行目の sha1(); が Hash.h ライブラリの関数です。

以上です。
こんな長いプログラムでもメモリが余裕ありありです。Arduino UNO ではとても無理ですね。
ESP-WROOM-02 ( ESP8266 ) はスゴイ!!

8.コンパイル

以前の記事でも掲載しましたが、WROOM(ESP8266)のGPIOピンのモードは

Pin No.Level
GPIO 15LOW
GPIO   2HIGH
GPIO   0LOW
ENHIGH

WroomForArduino_SSE_NTP10
このようにして、FLASHダウンロード(書き込み)モードにしてください。そしてコンパイルします。

また、スケッチを修正して再びコンパイルする時にコンパイルできない時があります。
その場合はUSBを抜き、GPIOピンをFlash 書き込みモードにしてUSBを再度差し込み、Arduino IDEの書き込みボタンをクリックしてください。その場合、1回目はエラーになりますが、もう一度書き込みボタンをクリックしてください。

9.スマホとコネクション

コンパイルが済んで、GPIOピン0番をHIGHにしてFlashブートモードにします。

WroomForArduino_SSE_NTP18

次にWROOMのUSBを挿して電源を入れます。
シリアルモニターを起動してからリセットボタンを押します。
すると以下のようなメッセージが出ればOKです。
WebSocket-wroom03-04

次に予めルーターとWi-Fi接続してあるスマホのブラウザを起動します。
ブラウザは最新版のGoogle Chrome か Safari にしてください。

次にURL入力欄にWROOMに割り当てられたローカルIPアドレスを入力します。
私の環境の場合は
192.168.0.21
のように入力してエンターを押します。

すると、数秒後にHTMLページが表示され、
CONNECTED
と出るとハンドシェイク完了です。
WebSocket-wroom03-06

赤い数値が300ms毎に変化していれば通信成功です。
スライダーを動かしてみてください。
明るさがリアルタイムに変化すればOKです。
稀に通信環境などでコネクション強制切断される場合がありますが、数十秒後に再びコネクションすれば接続できると思います。
「WS.CLOSE」ボタンを押すとブラウザ側から通信切断できます。
数十秒待って、再び接続することも可能です。
又は、別のスマホでコネクションすることも可能です。

どうでしょう、うまく動きましたでしょうか?
何か不明な点とかあればコメント欄やお問い合わせフォームでご連絡ください。
最近忙しくて素早い返答ができないかもしれませんがご容赦ください。

なかなか、WebSocketの記事を書くのは量が多くで疲れに疲れます・・・。
素直にアプリ開発した方が早かったかもしれませんね。・・・。
何が何でもブラウザでやる必要もないんですが・・・。
(すいません、単なるボヤキです)

と、いうことで今回はここまでです。
ではまた・・・。

最新記事ではWebSocket通信をライブラリ化して、格段に使いやすくなりました。
こちらのページをご覧ください。

EWS_Beta13_15

スポンサーリンク

Amazon.co.jp 当ブログのおすすめ

投稿者:

mgo-tec

Arduino , ESP32 ( ESP-WROOM-32 ) , ESP8266 ( ESP-WROOM-02 )等を使って、主にスマホと連携した電子工作やプログラミング記事を書いてます。ライブラリも作ったりしてます。趣味、独学でやってますので、動作保証はしません。 電子回路やプログラミングの専門家ではありません。 畑違いの仕事をしていて、介護にドップリ浸かりそうな年配者です。 少しだけ電気の知識が必要な仕事なので、電気工事士や工事担任者等の資格は持っています。

「WebSocket でスマホから Arduino化 WROOM のLEDを調光してみる」への4件のフィードバック

  1. 初めまして
    最近、Arduinoを始めてたばかりの初心者です。
    ”WROOM のLEDを調光”はなんとか成功しました。
    環境は下記です。
    タブレットNexus7 : Android Ver6.0.1
    PC:自作のWin10
    Arduino Ver : 1.8.1
    ESP8266互換機

    お聞きしたい事は山ほどありますが、あまりにも初歩すぎて ^^;
    これを参考に理解し勉強して、目的の事に挑戦したいと思います。
    ではでは!

    1. tamaさん

      当ブログをご覧いただき、ありがとうございます。
      動作して良かった・・・。
      ホッとしました。

      ここの記事はかなり昔に書いたもので、WebSocket もまだまだ不安定だと思います。
      最新記事では、ライブラリを自作して、かなり安定して動作します。

      EasyWebSocket ライブラリ Beta 1.51 ( ESP32 , ESP8266 )をアップしました

      インストール方法とか、SPIFFS など、いろいろと記事が散在していて申し訳ありませんが、是非試してみて下さいませ。

  2. 初めまして
    最近、Arduinoを始めたばかりの初心者です。

    数秒後にHTMLページの表示が
    CONNECTED
    にならず
    WS.Close.DisConnected
    と表示されます。

    ここまで色々自力でやってきましたが、
    あまりにも基礎知識がなく先に進めなさそうです。

    IDEからソフトは入り、
    ブラウザー画面までアクセスできました。

    ボード
    ESPr® Developer(ESP-WROOM-02開発ボード)

    どんなことでも構いません。
    何かヒントいただけると助かります。

    1. AKIRA さん

      記事をご覧いただき、ありがとうございます。

      まず、教えていただきたいのですが、ブラウザは何を使っておられますでしょうか?
      Microsoft EDGE や Internet Explore は動作しないと思われます。
      推奨は Google Chrome です。

      それと、高速CPU 大容量メモリを搭載しているスマホの方が有利です。
      古いスマホはよく途中で停止しました。

      iPhone などの Safari も動作しますが、あまり快適とは言えません。
      ただ、iPhone や iPad も最新の高速CPU をお勧めします。
      iOS も最新バージョンをお勧めします。

      ソースコードの23行目を100ms くらいインターバルを置くと停止しにくいです。
      s_rate = 100;
      ただ、その分反応は悪くなります。

      あと、このソースコードでは快適動作とはいかないと思いますので、EasyWebSocket ライブラリを使うことをお勧めします。
      最新バージョンは Beta 1.51.2 です。
      以下のページを参照してください。

      自作ライブラリ EasyWebSocket のインストール方法

コメントを残す

メールアドレスが公開されることはありません。

*画像の文字を入力してください。(スパム防止の為)

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください