2019年4月12日金曜日

ESP32/arduino:SPIFFSのテキストファイルをダウンロードする

目的:

SPIFFS上に保存した設定ファイル等のテキストファイルをダウンロードできる様にする。

方法:

  1. WEBクライアントから XMLHttpRequest で テキストファイルを要求。
  2. ESP32からテキストファイルの内容を応答。
  3. 応答されたテキストファイルから Blob オブジェクトを生成。
  4. Blobオブジェクトをファイルとして保存。
の順に実行。
ファイルとして保存する場合 IE/Edge と その他で異なり、
IE/Edge : msSaveOrOpenBlob を使用。( 又は msSaveBlob )
その他   : createObjectURL でBlobオブジェクトへのURL を生成し、そのURLへのリンク (a要素) を生成して JavaScript でクリックする。

例:

softAPでWiFi設定 の 設定ファイル をダウンロードできる様にする。

< 各画面サンプル >


WiFi設定画面
戻るボタンを追加
他は変更なし

LED調光の画面
「WiFi設定画面」ボタンの右側に
ダウンロード用のリンクを追加 


*2020/04/13 : レスポンス用HTML が 漏れていた為 追記。
  また、各HTML のファイル名追記。
*2020/05/13 : WiFi 接続不可時に APモードに戻す際、設定ファイルの更新が漏れていた為 更新と追記。(LED調光用 スケッチ 行#38,39)

<!DOCTYPE html><html lang='ja'><head><meta charset='UTF-8'>
<style>
    #base         {font-size:16pt; text-align:center; width:600px; border:solid 4px #00000000; background-color:#c0c0c0; }
    #text_box     {height: 25px; font-size:12pt; float:left ; text-align:left; width:45%;}
    #input_box    {height: 25px; font-size:12pt; float:right; text-align:left; width:55%;}
    #ip_box       {height: 25px; font-size:12pt; float:right; text-align:left; width:15%;}
    #radio_box    {font-size:12pt; clear:both; margin : 0% 20% ; width : 60% ;}
    #button       {font-size:12pt; clear:both; width 50%}
    #foot         {font-size:16pt; clear:both;}
    input.val     {width: 90%;}
    input.ip      {width: 20%;}
    input.button  {margin:10px 10% ; width: 25%;}
    input.radio   {margin:10px 0px 0px 15% ; }
</style>

<title>設定画面</title></head>

<body>
<div id="base">
  <p>設定画面</p>
  <div id="text_box">
    <span> WiFi 接続先 SSID </span>
  </div>
 <form method="get">
  <div id="input_box">
      <input class='val' type='text' name='ssid' id='ssid' value=$ssid>
  </div>
  <div id="text_box">
    <span> WiFi 接続先 PASSWORD </span>
  </div>
  <div id="input_box">
      <input class='val' type='text' name='pass' id='pass' value=$pass>
  </div>
  <div id="text_box">
    <span> WiFi 接続  IP アドレス </span>
  </div>
  <div id="input_box">
      <input class='ip' type='number' name='ip1' id='ip1' min=0 max=255 value=$ip1 >
      <input class='ip' type='number' name='ip2' id='ip2' min=0 max=255 value=$ip2 >
      <input class='ip' type='number' name='ip3' id='ip3' min=0 max=255 value=$ip3 >
      <input class='ip' type='number' name='ip4' id='ip4' min=0 max=255 value=$ip4 >
  </div>
  <div id="text_box">
    <span> WiFi 接続  サブネットマスク</span>
  </div>
  <div id="input_box">
      <input class='ip' type='number' name='sn1' id='sn1' min=0 max=255 value=$sm1 >
      <input class='ip' type='number' name='sn2' id='sn2' min=0 max=255 value=$sm2 >
      <input class='ip' type='number' name='sn3' id='sn3' min=0 max=255 value=$sm3 >
      <input class='ip' type='number' name='sn4' id='sn4' min=0 max=255 value=$sm4 >
  </div>
  <div id="text_box">
    <span> WiFi 接続  デフォルトゲートウェイ</span>
  </div>
  <div id="input_box">
      <input class='ip' type='number' name='gw1' id='gw1' min=0 max=255 value=$gw1 >
      <input class='ip' type='number' name='gw2' id='gw2' min=0 max=255 value=$gw2 >
      <input class='ip' type='number' name='gw3' id='gw3' min=0 max=255 value=$gw3 >
      <input class='ip' type='number' name='gw4' id='gw4' min=0 max=255 value=$gw4 >
  </div>
  <div id="text_box">
    <span> WiFi 接続  DNSアドレス</span>
  </div>
  <div id="input_box">
      <input class='ip' type='number' name='dns1' id='dns1' min=0 max=255 value=$dns1 >
      <input class='ip' type='number' name='dns2' id='dns2' min=0 max=255 value=$dns2 >
      <input class='ip' type='number' name='dns3' id='dns3' min=0 max=255 value=$dns3 >
      <input class='ip' type='number' name='dns4' id='dns4' min=0 max=255 value=$dns4 >
  </div>
  <div id="radio_box">
      <input class='radio' type='radio' name='wifi_stamode' id='rad_sta' value='sta' $checked_sta > STA MODE
      <input class='radio' type='radio' name='wifi_stamode' id='rad_ap' value='ap' $checked_ap > AP MODE
  </div>
  <div id="button">
      <input class='button' type='submit' name='set' id='set' value='設定'>
      <input class='button' type='submit' name='rtn' id='rtn' value='戻る'>
  </div>
 </form>
 <div id="foot">
   <span>$footer</span>
 </div>
</div>
</body>
</html>

<!DOCTYPE html><html lang='ja'><head><meta charset='UTF-8'>
<style>
    #base       {font-size:16pt; text-align:center; width:400px; border:solid 4px #93ff93; background-color:#f0f0f0; }
    #btn_onoff  {font-size:12pt; text-align:center; width:100%; }
    #btn_conf   {font-size:12pt; text-align:left  ; width:83%; }
    #button     {font-size:12pt; clear:both; width 50%}
    #text_in    {height: 12pt ; margin:14px 0px 0px 5px ; font-size:8pt; float:right; }
    #foot       {font-size:16pt; clear:both;}
    input       {margin:8px; text-align:left ; width:100px;border: }
    input.button  {margin:6px 0px 0px 90px ; text-align:left ; width:100px;}
    div         {font-size:16pt;text-align:center;}
</style>

<title>Color LED Controller</title></head>

<body>
<div id="base">
  <p>LED ON/OFF</p>
  <div id="btn_onoff">
    <form method='get'>
      <input type='submit' name='on' value='ON' />
      <input type='submit' name='off' value='OFF' />
    </form>
  </div>
  <div id="btn_conf">
    <form method="get">
        <input class='button' type='submit' name='config' id='config' value='WiFi設定画面'>
        <div id="text_in">
          <a href="#" onclick='cnfdl();'>設定をダウンロードする</a>
        </div>
     </form>
  </div>
  <br/>
 <div id="foot">
   <span>$footer</span>
 </div>
</div>

  <script>
function cnfdl() {
  "use strict";

  var xhr = new XMLHttpRequest();
  xhr.open("get", "?cnfld");
  xhr.setRequestHeader('Cache-Control', 'no-cache');
  xhr.setRequestHeader('If-Modified-Since', 'Thu, 01 Jun 1970 00:00:00 GMT');
  xhr.responseType = 'document' ;
 
  xhr.onreadystatechange = function() {
    if( (xhr.readyState == 4) && (xhr.status == 200) ) {
      var filename = "config.txt" ;
      var str = xhr.response.getElementById("output").innerHTML ;
      var blob = new Blob([str],{type:'text/plain'}) ;
      if (window.navigator.msSaveBlob) {
      //window.navigator.msSaveBlob(blob, filename);
        window.navigator.msSaveOrOpenBlob(blob,filename) ;
      }
      else {
        var objectURL = window.URL.createObjectURL(blob);
        var link = document.createElement("a");
        document.body.appendChild(link);
        link.href = objectURL;
        link.download = filename;
        link.click();
        document.body.removeChild(link);
      }
    }
  }
  xhr.send();
}
  </script>
</body>
</html>

<!DOCTYPE html><html lang='ja'>
  <head> <title>Color LED Controller</title></head>
  <html> <body>
    <output id='output'>$cnf_text</output>
  </body> </html>

( 2020/05/02 *client.available() にならない場合のタイムアウト処理を追加したスケッチ(メイン)の 例を 「クライアントのタイムアウト」に記載。  )
#include <WiFi.h>                         // WiFi 使用の為
#include "FS.h"                           // SPIFFS 使用の為
#include "SPIFFS.h"                       // SPIFFS 使用の為

#define DEBUG_HTML                        // Debug用 Serial 表示制御用
#define DEBUG                             // Debug用 Serial 表示制御用

// AP モード用 WIFI 設定 ---------------------------------------------
const char ssid[] = "ESP32_AP";           // SSID for softAP
const char pass[] = "password";           // password for softAP
const IPAddress ip(192, 168, 4, 1);       // IPアドレス for softAP
const IPAddress subnet(255, 255, 255, 0); // サブネットマスク for softAP

// 動作モードフラグ ---------------------------------------------------
bool stamode = true ;                     // WiFi STA 接続モード : true
bool sta_exec = false ;                   // WiFi STA接続モード実行中フラグ
bool config_mode  = false ;               // 初期設定モード ; true
bool config_exec = false ;                // 初期設定実行中フラグ

// 設定ファイル用 -----------------------------------------------------
String s_config ;                         // 設定ファイル用 String
char wifi_ssid[128] = "" ;                // SSID        for WiFi STA
char wifi_pass[128] = "" ;                // password    for WiFi STA
IPAddress wifi_ip(192,168,1,66) ;         // IP Address  for WiFi STA
IPAddress wifi_gw(192,168,1,1)  ;         // gate way    for WiFi STA
IPAddress wifi_sm(255,255,255,0) ;        // subnet mask for WiFi STA
IPAddress wifi_dns(192,168,1,1) ;         // dns address for WiFi STA
char wifi_mode[16] = ""   ;               // wifi mode
String foot_msg ="" ;                     // リブートメッセージ用

WiFiServer server(80);

String html_CONF ;                        // 設定用 HTML
String html_MAIN ;                        // メイン HTML
String html_resp ;

// LED 調光処理用 ----------------------------------------------------------------
// -- for ledc
byte brightness = 0 ;                     // LED 明るさ設定 (0-255)

// -----------------------------------------------------------------------------
//  arduino 初期化処理                                                        --
// -----------------------------------------------------------------------------
void setup() {
  int res = 0 ;                                // 結果格納用 (ワーク)

  Serial.begin(115200);                        // シリアル 開始
  SPIFFS.begin(true) ;                         // SPIFFS 開始

  // 初期設定用 HTML ファイルの読み込み ---------------------------------------
  res = rd_SPIFFS("/WiFi_Config.html",html_CONF) ;  // html_CONF に Config.html を格納

  // 本体 HTML ファイルの読み込み ----------------------------------------------
  res = rd_SPIFFS("/main.html",html_MAIN) ;         // html_MAIN に main.html を格納
  
  // レスポンス用HTML ファイルの読み込み ----------------------------------------
  res = rd_SPIFFS("/resp_cnfld.html",html_resp) ;   // html_resp に resp_confld.html を格納
  
  // 初期設定ファイルを読み込み、グローバル変数に設定し、wifi,画面モードを決定 ---
  res = rd_config() ;                          //

  // wifiモード(stamode) に応じて AP か STA かを選択してサーバー起動 -------------
  if (stamode == false) {                    // APモードの場合 softAP WiFi 開始
    start_AP_server() ;                      // APモードでサーバー起動
    sta_exec = false ;                       //
  } else {                                   // STAモードの場合 Wifi 開始
    start_STA_server() ;                     // APモードでサーバー起動
    sta_exec = true ;                        //
  }                                          //

  // 実行中のモード (設定かメインか) を設定 -------------------------------------
  if (config_mode) config_exec = true ;   // config_mode なら config_exec を '1'
  else config_exec = false ;              //

  Serial.println("Server start!");

  // メイン処理用 初期化 ------------------------------------------------------
  ledcSetup(0, 5000, 8);            // channel 0 周波数 5000 Hz, 8 bit 
  ledcAttachPin(2,0);               // attach pin 2 to channel 0
  ledcWrite(0, 0);                  // initialize channel 0 to off     


}
// -----------------------------------------------------------------------------


// -----------------------------------------------------------------------------
//  arduino メインループ処理                                                  --
// -----------------------------------------------------------------------------
void loop() {
  WiFiClient client = server.available();
  String htmlwk = "" ;                            // html 用 ワーク変数
  String line   = "" ;                            // クライアントからの入力用
  int xhr = 0 ;                                   // xhr 要求フラグ
 
  // HTML クライアント処理 ------------------------------------------------------
  if (client) {
    Serial.println(" +++++++++++++++++ new client! +++++++++++++++++ ");
    while (client.connected()) {                  // クライアントから接続されたとき
      if (client.available()) {                   //
        line = client.readStringUntil('\n');      // 1行分取得
        # ifdef DEBUG_HTML                        //
          Serial.println(line) ;                  //
        # endif                                   //
        if (line.indexOf("GET /?") != -1) {       // GET 処理
          if (config_mode)                        // 初期設定用 フォームデータ処理
            set_form2param(line) ;                //   フォームデータを変数に格納
          else {                                  // メイン用 処理
            xhr = proc_main(line) ;               //   フォームデータのメイン処理
          }                                       //
        }                                         //
        // 最終行(空行)を受信した時 -------------------------------------------
        if (line.length() == 1 && line[0] == '\r'){  // 最終行判定
          if (stamode != sta_exec) {         // sta_mode が変わった場合
            if (strcmp(wifi_mode,"ap") == 0){    //  リブートメッセージを設定
              foot_msg = "アクセスポイントモードに移行します。<br> " ;
              foot_msg += "この画面を閉じてアクセスポイントに接続して下さい。" ;
            }else{
              foot_msg = "ステーションモードに移行します。<br>"; 
              foot_msg += "この画面を閉じて 設定したアドレスに接続して下さい。" ;
            }
            if (config_exec) {                    //   初期設定中なら
              send_CONF_html(client) ;            //     初期設定 HTML 送信
            } else {                              //   メイン画面なら
              send_MAIN_html(client) ;            //     メイン HTML 送信
            }                                     //    ( その後 リブート)
          } else {                                // sta_mode が変わらない場合 
            if (config_mode) {                    //   次に初期設定を表示するなら
              send_CONF_html(client) ;            //     初期設定 HTML 送信
              config_exec = true ;                //
            } else {                              //   次にメイン画面なら
              if (xhr) {                          //     XHR 応答の場合
                send_resp_html(client) ;          //       XHR 応答 送信
              } else {                            //     HTML 送信なら
                send_MAIN_html(client) ;          //     メイン HTML 送信
                config_exec = false ;             //
              }                                   //
            }                                     // 
          }                                       //
          # ifdef DEBUG_HTML                      //
            Serial.println("Send HTML") ;         //
          # endif                                 //
          break ;                                 // ループ終了 
        }                                         //
        //-------------------------------------------------------------------
      } 
    }
    // 接続が切れた場合 ------------------------------------------------------
    client.stop();
    Serial.println("client disonnected");
    Serial.println("----------------------------------------------------");
  }

  if (stamode != sta_exec) {                      // sta_mode が変わった場合
    Serial.println("------------------- リブートします------------------- ");
    delay(500);                                   // 
    ESP.restart() ;                               // リブート
  } else {
  }
  // --------------------------------------------------------------------------

}

// *****************************************************************************
// *  初期設定ファイル をリードし、グローバル変数に値をセットする              *
// *****************************************************************************
int rd_config() {                                //
  File fp       ;                                // 設定ファイル用ファイルポインタ
  int result = 0 ;                               // 戻り値
                                                 //
  if (SPIFFS.exists("/config.txt")) {            // 設定ファイル存在確認
    // ファイルがあった場合 ---------------------//
    result = rd_SPIFFS("/config.txt",s_config) ; // s_config に config.txt を格納
  } else {                                       //
    // ファイルが無かった場合 -------------------//
    Serial.println("設定ファイルなし") ;         // 無かったら、APモード
    result = -1 ;                                //
  }                                              //

  // ファイルが読み込めたら、グローバル変数にセットする
  if (result == 0)                               //
    # ifdef DEBUG                                // デバッグ用表示
      Serial.println("--- s_config --- ") ;      //
      Serial.println(s_config ) ;                //
    # endif                                      //
    set_conf2param() ;                           // s_config の内容を変数に設定
  // 初期設定ファイルの状態で wifiモード, 画面モードを設定 ----------------------
  if (result != -1) {                          // 変数に設定できた場合
    if (( strcmp(wifi_ssid,"") ==0) || (strcmp(wifi_pass,"") ==0)) {
                                               // ssid,pass 設定なければ
      stamode = false ;                        //   APモード
      if ( strcmp(wifi_mode,"ap") == 0)        // wifi_mode が "ap" なら
        config_mode = false ;                  //   メイン画面
      else                                     // wifi_mode が "ap" でなければ
        config_mode = true ;                   //   初期設定画面
    } else {                                   // ssid,pass の設定があれば、
      config_mode = false ;                    //   メイン画面
      if ( strcmp(wifi_mode,"ap") == 0)        // wifi_mode が "ap" なら
        stamode = false ;                      //   APモード
      else                                     // "ap" でなければ
        stamode = true  ;                      //   STAモード
    }
  } else {                                     // 変数に設定できなかった場合
    stamode = false ;                          //   APモード
    config_mode  = true ;                      //   初期設定画面
  }

  return result ;                              //
}

// ----------------------------------------------------------------------------
// - 設定ファイルの内容を グローバル変数に 設定する                           -
// ----------------------------------------------------------------------------
void set_conf2param() {
  int    pos = 0 ;
  int    npos = 0 ;
  String s_work ;
  String s_name ;
  String s_data ;
  // 記載内容を順次処理 ( 設定情報を取り出し、グローバル変数に設定する ) ------
  while(1) {
    if (s_config.charAt(pos) == '/') {         // 先頭が '/' なら 次の行へ 
      npos = s_config.indexOf('\n',pos) ;      //
      pos = npos + 1 ;                         //
    } else {                                   //
      npos = s_config.indexOf(':',pos) ;       // ':' までの文字をs_name に取得
      if (npos == -1) break ;                  //   見つからなければ終了
      s_name = s_config.substring(pos,npos) ;  //
      s_name.trim() ;                          //
      pos = npos+1 ;                           // ポインタを ':' の次へ
      npos = s_config.indexOf('\n',pos) ;      // '\n' までの文字をs_data に取得
      if (npos == -1) npos = s_config.length() ;  //
      s_data = s_config.substring(pos,npos) ;  //
      s_data.trim() ;                          //
      pos = npos+1 ;                           //
      set_param(s_name,s_data) ;               // 取得した内容をグローバル変数に設定
    }                                          //
  }                                            //
}                                              //

// - 設定内容を判定し、グローバル変数に設定 ------------------------------------
void set_param(String &name, String &data) {   //
  if (name.compareTo("wifi_ssid")==0) {        // 'wifi_ssid' の場合
    data.toCharArray(wifi_ssid,128) ;          //
  }                                            //
  if (name.compareTo("wifi_pass")==0) {        // 'wifi_pass' の場合
    data.toCharArray(wifi_pass,128) ;          //
  }                                            //
  if (name.compareTo("wifi_ip")==0) {          // 'wifi_ip' の場合
    wifi_ip = stoip(data) ;                    //  取得情報(文字列)をIPAddressに変換
  }                                            //
  if (name.compareTo("wifi_gw")==0) {          // 'wifi_gw' の場合
    wifi_gw = stoip(data) ;                    //
  }                                            //
  if (name.compareTo("wifi_sm")==0) {          // 'wifi_sm' の場合
    wifi_sm = stoip(data) ;                    //
  }                                            //
  if (name.compareTo("wifi_dns")==0) {         // 'wifi_dns' の場合
    wifi_dns = stoip(data) ;                   //
  }                                            //
  if (name.compareTo("wifi_mode")==0) {        // 'wifi_mode' の場合
    data.toCharArray(wifi_mode,16) ;           //
  }                                            //
}                                              //

// - 文字列 から IPAddress へ変換 ----------------------------------------------
IPAddress stoip(String &data) {                 //
  IPAddress ip ;
  String num = "" ;
  int pos = 0 ;
  while(data.charAt(pos) != '.') {             // 先頭から '.' までの文字を取得
    num += data.charAt(pos++) ;                //
  }                                            //
  ip[0] = num.toInt() ;                        // 数値に変換して 第1オクテット に 代入
  pos++ ;                                      // '.' の次から
  num="";                                      //
  while(data.charAt(pos) != '.') {             // '.' までの文字を取得
    num += data.charAt(pos++) ;                //
  }                                            //
  ip[1] = num.toInt() ;                        // 数値に変換して 第2オクテットに代入
  pos++ ;                                      // '.' の次から
  num="";                                      //
  while(data.charAt(pos) != '.') {             // '.' までの文字を取得
    num += data.charAt(pos++) ;                //
  }                                            //
  ip[2] = num.toInt() ;                        //
  pos++ ;                                      // '.' の次から
  num="";                                      // 
  while(data.charAt(pos) != '.') {             // '.' または 最後まで の 文字を取得
    num += data.charAt(pos++) ;                //
    if (pos > data.length()) break ;           //
  }                                            //
  ip[3] = num.toInt() ;                        // 数値に変換して 第4オクテットに代入
                                               //
  return ip ;                                  // IPAddress を戻り値とする
}                                              //

// ****************************************************************************
// * アクセスポイント モードで サーバーを起動                                 * 
// ****************************************************************************
void start_AP_server() {
  Serial.println(" AP Server exec") ;
  WiFi.softAP(ssid, pass);             // SSIDとパスの設定
  delay(100);                          // delayが必要
  WiFi.softAPConfig(ip, ip, subnet);   // IP address, gateway, subnetmask の設定
  IPAddress myIP = WiFi.softAPIP();    // WiFi.softAPIP()でWiFi起動
  server.begin();                      // サーバーを起動(htmlを表示させるため)
}

// ****************************************************************************
// * デバッグ表示用                                                           *
// ****************************************************************************
void disp_mode() {
  # ifdef DEBUG_HTML                              // デバッグ用表示
    Serial.print("config_mode - exec : ") ;       // 設定画面か メイン画面か
    if (config_mode)                              // 変数の状態と実行状況を表示
      Serial.print("Config MODE - ") ;            //
    else                                          //
      Serial.print("Main MODE - ") ;              //
    if (config_exec)                              //
      Serial.println("Config MODE") ;             //
    else                                          //
      Serial.println("Main MODE") ;               //
                                                  //
    Serial.print("wifi_mode - exec   : ") ;       // Wifi モードを
    if (stamode)                                  // 変数の状態と実行状況を表示
      Serial.print("STA - ") ;                    //
    else                                          //
      Serial.print("AP - ") ;                     //
    if (sta_exec)                                 //
      Serial.println("STA") ;                     //
    else                                          //
      Serial.println("AP") ;                      //
  # endif                                         //
}

// ****************************************************************************
// * クライアントからのフォームデータを変数にセットし、設定ファイルに書き出す *
// ****************************************************************************
void set_form2param(String &line) {
  String s_work ="" ;

  if (line.indexOf("set=") != -1) {               // stamode set
    // クライアントからのデータを グローバル変数にセットする。 ------------------
    if (line.indexOf("GET /?ssid=") != -1) {        // wifi_ssid
      s_work = getvalue_s(line,"?ssid=") ;          //   ssid に続く文字列を取得
      s_work.toCharArray(wifi_ssid,128) ;           //   変数にセット
    }
    if (line.indexOf("pass=") != -1) {              // wifi_pass
      s_work = getvalue_s(line,"pass=") ;           //   pass に続く文字列を取得
      s_work.toCharArray(wifi_pass,128) ;           //   変数にセット
    }
    if (line.indexOf("ip1=") != -1) {               // wifi_ip
      wifi_ip[0] = getvalue_i(line,"ip1=") ;        //   ip1 に続く数値を変数にセット
    }
    if (line.indexOf("ip2=") != -1) {               // 
      wifi_ip[1] = getvalue_i(line,"ip2=") ;        //   ip2 に続く数値を変数にセット
    }
    if (line.indexOf("ip3=") != -1) {               // 
      wifi_ip[2] = getvalue_i(line,"ip3=") ;        //   ip3 に続く数値を変数にセット
    }
    if (line.indexOf("ip4=") != -1) {               // 
      wifi_ip[3] = getvalue_i(line,"ip4=") ;        //   ip4 に続く数値を変数にセット
    }
    if (line.indexOf("gw1=") != -1) {               // wifi_gw
      wifi_gw[0] = getvalue_i(line,"gw1=") ;        //   gw1 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("gw2=") != -1) {               //                       
      wifi_gw[1] = getvalue_i(line,"gw2=") ;        //   gw2 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("gw3=") != -1) {               //                       
      wifi_gw[2] = getvalue_i(line,"gw3=") ;        //   gw3 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("gw4=") != -1) {               //                       
      wifi_gw[3] = getvalue_i(line,"gw4=") ;        //   gw4 に続く数値を変数にセット
    }
    if (line.indexOf("sm1=") != -1) {               // wifi_sm
      wifi_sm[0] = getvalue_i(line,"sm1=") ;        //   sm1 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("sm2=") != -1) {               //                       
      wifi_sm[1] = getvalue_i(line,"sm2=") ;        //   sm2 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("sm3=") != -1) {               //                       
      wifi_sm[2] = getvalue_i(line,"sm3=") ;        //   sm3 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("sm4=") != -1) {               //                       
      wifi_sm[3] = getvalue_i(line,"sm4=") ;        //   sm4 に続く数値を変数にセット
    }
    if (line.indexOf("dns1=") != -1) {              // wifi_dns
      wifi_dns[0] = getvalue_i(line,"dns1=") ;      //   dns1 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("dns2=") != -1) {              //                       
      wifi_dns[1] = getvalue_i(line,"dns2=") ;      //   dns2 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("dns3=") != -1) {              //                       
      wifi_dns[2] = getvalue_i(line,"dns3=") ;      //   dns3 に続く数値を変数にセット
    }                                                                        
    if (line.indexOf("dns4=") != -1) {              //                       
      wifi_dns[3] = getvalue_i(line,"dns4=") ;      //   dns4 に続く数値を変数にセット
    }
    if (line.indexOf("stamode=") != -1) {           // stamode set
      s_work = getvalue_s(line,"stamode=") ;        //   ssid に続く文字列を取得
      s_work.toCharArray(wifi_mode,16) ;            //   変数にセット
      if (s_work == "sta") {                        //   設定値により、モードをセット
        stamode = true ;                            // 
        config_mode = false ;                       // 
      } else {                                      // 
        stamode = false ;                           // 
        config_mode = false ;                       // 
      }
    }
    // グローバル変数を 設定ファイルに書き出す -----------------------------------
    wr_config() ;                           // 設定を初期設定ファイルに書き出し
  }
  if (line.indexOf("rtn") != -1) {                  // return
    config_mode = false ;                           // 
  }
}

// 要素名に続く値(文字列)を取得する --------------------------------------------
String getvalue_s(String &line,String param) {
  String val="" ;
  int pos = 0 ;

  if ((pos=line.indexOf(param)) != -1) {          // 要素名の位置を取得
    pos += param.length() ;                       // 
    while((line.charAt(pos) != '&') & (line.charAt(pos) != '\n')) {
      val += line.charAt(pos++) ;                 //  '&' か 行末 まで文字を取得
    }                                             //
  }                                               //
  return val ;                                    //  取得した文字列を返す
}

// 要素名に続く値(数値)を取得する ---------------------------------------------
int getvalue_i(String &line,String param) {
  String val="" ;
  int pos = 0 ;

  if ((pos=line.indexOf(param)) != -1) {          // 要素名の位置を取得
    pos += param.length() ;                       //
    while((line.charAt(pos) >= '0') & (line.charAt(pos) <= '9')) {
      val += line.charAt(pos++) ;                 //  数値でなくなるまで文字を
    }                                             //  取得
  }                                               //
  return val.toInt() ;                            //  数値に変換して返す
}

// -----------------------------------------------------------------------------
// - グローバル変数の内容を設定ファイルに書き出す                              -
// -----------------------------------------------------------------------------
void wr_config() {
  char s_work[128] ;
  File fp ;

  // String に 変数の内容を書き込む -------------------------------------------
  sprintf(s_work,"wifi_ssid : %s\n",wifi_ssid) ;  //
  s_config = String(s_work) ;                     //
  sprintf(s_work,"wifi_pass : %s\n",wifi_pass) ;  //
  s_config += String(s_work) ;                    //
  sprintf(s_work,"wifi_ip   : %3d.%3d.%3d.%3d\n",wifi_ip[0],wifi_ip[1],wifi_ip[2],wifi_ip[3]) ;
  s_config += String(s_work) ;                    //
  sprintf(s_work,"wifi_sm   : %3d.%3d.%3d.%3d\n",wifi_sm[0],wifi_sm[1],wifi_sm[2],wifi_sm[3]) ;
  s_config += String(s_work) ;                    //
  sprintf(s_work,"wifi_gw   : %3d.%3d.%3d.%3d\n",wifi_gw[0],wifi_gw[1],wifi_gw[2],wifi_gw[3]) ;
  s_config += String(s_work) ;                    //
  sprintf(s_work,"wifi_dns  : %3d.%3d.%3d.%3d\n",wifi_dns[0],wifi_dns[1],wifi_dns[2],wifi_dns[3]) ;
  s_config += String(s_work) ;                    //
  sprintf(s_work,"wifi_mode : %s\n",wifi_mode) ;  //
  s_config += String(s_work) ;                    //

  // 設定ファイルに書き込む ----------------------------------------------------
  fp = SPIFFS.open("/config.txt",FILE_WRITE) ;    // 設定ファイルを開く
  if (!fp) {
    Serial.println(" 設定ファイル オープンエラー !!") ;
  } else {
    // 初期設定ファイル書き込み -------------------------------------------------
    if(fp.print(s_config)) {                      // ファイルに書き込み
      Serial.println("s_config written") ;        //   終了メッセージ
    } else {                                      //
      Serial.println("s_config write error !!") ; //   失敗メッセージ
    }                                             //
    fp.close() ;                                  // ファイルクローズ
  }                                               //
                                                  //
}                                                 //

// *****************************************************************************
// * HTML 送信処理                                                             *
// *****************************************************************************

// ----------------------------------------------------------------------------
// - CONF_HTML 送信  -----------------------------------------------------------
// ----------------------------------------------------------------------------
void  send_CONF_html(WiFiClient client) {
  String htmlwk ;

  // 変数置換え処理 ------------------------------------------------------------
  htmlwk = html_CONF ;
  htmlwk.replace("$ssid",String(wifi_ssid)) ;
  htmlwk.replace("$pass",String(wifi_pass)) ;
  htmlwk.replace("$ip1",String(wifi_ip[0])) ;
  htmlwk.replace("$ip2",String(wifi_ip[1])) ;
  htmlwk.replace("$ip3",String(wifi_ip[2])) ;
  htmlwk.replace("$ip4",String(wifi_ip[3])) ;
  htmlwk.replace("$sm1",String(wifi_sm[0])) ;
  htmlwk.replace("$sm2",String(wifi_sm[1])) ;
  htmlwk.replace("$sm3",String(wifi_sm[2])) ;
  htmlwk.replace("$sm4",String(wifi_sm[3])) ;
  htmlwk.replace("$gw1",String(wifi_gw[0])) ;
  htmlwk.replace("$gw2",String(wifi_gw[1])) ;
  htmlwk.replace("$gw3",String(wifi_gw[2])) ;
  htmlwk.replace("$gw4",String(wifi_gw[3])) ;
  htmlwk.replace("$dns1",String(wifi_dns[0])) ;
  htmlwk.replace("$dns2",String(wifi_dns[1])) ;
  htmlwk.replace("$dns3",String(wifi_dns[2])) ;
  htmlwk.replace("$dns4",String(wifi_dns[3])) ;
  if (stamode) {                                // ステーションモード時
    htmlwk.replace("$checked_sta","checked") ;  // 
    htmlwk.replace("$checked_ap","") ;          //
  } else {                                      // アクセスポイントモード時
    htmlwk.replace("$checked_sta","") ;         //
    htmlwk.replace("$checked_ap","checked") ;   //
  }
  htmlwk.replace("$footer",foot_msg ) ;
  // --------------------------------------------------------------------------

  send_html(client,htmlwk) ;                       // HTML 送信処理
}

// *****************************************************************************
// * SPIFFS ファイルを String に読み込む                                       *
// *****************************************************************************
int rd_SPIFFS(String fname, String &sname) {     //
  File fp = SPIFFS.open(fname,"r") ;             // ファイルをオープンする
  if (!fp) {                                     //  オープン失敗の場合
    Serial.print(fname) ;                        //
    Serial.println(" : file open error") ;       //
    return -1 ;                                  //    戻り値 -1
  }else {                                        //  オープン出来たら
    sname = fp.readString() ;                    //    String変数に読み込む
    fp.close() ;                                 //
  }                                              //
  return 0 ;                                     //  オープン出来た時のみ
}                                                //    戻り値 0
                                                 //
// ****************************************************************************
// * ステーションモードで サーバーを起動                                      * 
// ****************************************************************************
void start_STA_server() {
  int lpcnt = 0 ;                                    // ループカウント
  int lpcnt2 = 0 ;                                   // ループカウント2

  WiFi.config(wifi_ip, wifi_gw, wifi_sm, wifi_dns);  // Set fixed IP address
  delay(10) ;                                        //
  WiFi.begin(wifi_ssid, wifi_pass);                  // wifi 開始
  lpcnt = 0 ;                                        // 
  lpcnt2 = 0 ;                                       //
  while (WiFi.status() != WL_CONNECTED) {            // 接続確認
      lpcnt += 1 ;                                   //
      if (lpcnt > 4) {                               //
        WiFi.begin(wifi_ssid, wifi_pass);            // 4回目(2秒) で再度開始指示
        lpcnt = 0 ;                                  //
        lpcnt2 += 1 ;                                //
      }                                              //
      if (lpcnt2 > 5) {                              // 2秒x5 接続できなければ、
        stamode = false ;                            // APモードにして
        strcpy(wifi_mode, "ap") ;                    // widi_mode を AP にして
        wr_config() ;                                // 設定を初期設定ファイルに書き出し
        ESP.restart() ;                              // 再起動
      }                                              //
      Serial.print(".");                             //
      delay(500);                                    //   0.5秒毎にチェック
  }                                                  //
  server.begin();                                    // サーバー開始
}

// *****************************************************************************
// * HTML 送信処理                                                             *
// *****************************************************************************

// ----------------------------------------------------------------------------
// - CONF_HTML 送信                                                           -
// ----------------------------------------------------------------------------
void send_MAIN_html(WiFiClient client) {
  String htmlwk ;

  htmlwk = html_MAIN ;                          // htmlwk に HTML をコピー
  htmlwk.replace("$footer",foot_msg ) ;         // 変数を値に置き換える

  send_html(client,htmlwk) ;                    // HTML 送信処理

}

// ----------------------------------------------------------------------------
// - HTML 送信処理                                                            -
// ----------------------------------------------------------------------------
void send_html(WiFiClient client, String &html ) {
    client.println("HTTP/1.1 200 OK");           //
    client.println("Content-type:text/html");    //
    client.println();                            //
                                                 //
    client.print(html) ;                         //

    # ifdef DEBUG                                //
        Serial.println( " --- send html --- ");  //
    #endif                                       //
}

// ----------------------------------------------------------------------------
// - XHR RESPONSE_HTML 送信                                                   -
// ----------------------------------------------------------------------------
void send_resp_html(WiFiClient client) {        //
  String htmlwk ;                               // HTML 編集用 ワーク
                                                //
  htmlwk = html_resp ;                          // htmlwk に HTML をコピー
                                                //
  // 変数を値に変換-----------------------------//
  htmlwk.replace("$cnf_text",s_config) ;        // 変数に 設定文字列をセット
                                                //
  send_html(client,htmlwk) ;                    // HTML 送信処理
      Serial.print("xhr html  :"); Serial.println(htmlwk);
# ifdef DEBUG_WiFi                              //
      Serial.print("xhr value :"); Serial.println(duty);
#endif                                          //
}                                               //
                                                //

// *****************************************************************************
// * メイン処理                                                                *
// *****************************************************************************
int  proc_main(String &line) {
  String s_work ="" ;
  int xhr = 0 ;

  if (line.indexOf("GET /?config=") != -1) {        // config
    Serial.println(" -- Set config_mode truw--") ;
    config_mode = true ;
  }
  if (line.indexOf("GET /?on") != -1) {             // "on"が押下された時 
    brightness +=25 ;                               // 明るさを +25 
    Serial.print("brightness value : ");
    Serial.println(brightness);
    // -- for ledc
    ledcWrite(0, brightness) ;                      // チャネル#0 に Duty値設定
  }
  if (line.indexOf("GET /?off") != -1) {            // "off"が押下された時
    brightness = 0 ;                                // 明るさを "0" 
    Serial.print("brightness value : ");
    Serial.println(brightness);
    // -- for ledc
    ledcWrite(0, brightness) ;                      // チャネル#0 に Duty値設定
  }
  if (line.indexOf("GET /?cnfld") != -1) {          // config.txt のダウンロード時
    xhr = 1 ;
  }

  return xhr ;
}

// -----------------------------------------------------------------------------

以下、覚書など。(softAPでWiFi設定 からの変更点)

< WiFi 設定画面用 HTML >
(行#76)
 戻るボタン追加

< LED調光用 HTML >
(行#2~12)
スタイルを調整 (他行も併せて修正)
(行# 28~30)
 ダウンロード用リンクを追加。
クリックすると、JavaScript ( 'cnfdl();' ) を実行。この為、href は "#"。
(行#39~71)
ダウンロード用 JavaScript。
XHR で "?cnfld" を サーバ(ESP32) へ送信。
応答があれば、
1. 応答(文字列) を str に格納。
2. blob オブジェクトを生成。
3. ファイルの保存 又は ファイルを開く を実行。
(IE の場合)
window.navigator.msSaveOrOpenBlob でファイルの保存 又は 開く を実行。
(window.navigator.msSaveBlob なら ファイルの保存 )
(その他の場合)
(1) blob の URL を生成。
(2) リンク (a 要素) を生成
(3) リンクをクリック

 <レスポンス用HTML>(行#4)
  設定ファイル名(文字列) を送信する。
 $cnf_text は LED調光用スケッチ で 設定ファイルの文字列に置き換えられる。

 <メイン スケッチ>
(行#35)
 XHR 応答用 HTML の文字列格納用変数定義
(行#56,57)
XHR 応答用 HTML 読み込み
(行#94,109)
XHR 応答かどうかのフラグを追加
 (行#132~137)
XHR応答の場合の処理を追加

<WiFi 設定用 スケッチ>
 (行#180,251~254)
戻るボタン用の処理を追加

<LED調光用 スケッチ>
 (行#78~94)
XHR 応答 HTML 送信。
変数用の文字列  $cnf_text を 設定ファイルの文字列 に 置換して送信。
 (行#100,102)
メイン処理に戻り値(xhr) を追加。
クライアントからの get が ?cnfld の時、xhr = 1 で 戻る。

19 件のコメント:

  1. ご無沙汰です。
    あれから頑張って勉強を続けています。
    そこで、ファイル処理の関係を勉強しようと思って準備しています。
    少し質問ですが、WiFi 設定画面用 HTMLをWiFi_Config.htmlで保存
    LED調光用 HTMLがmain.htmlですね。
    あとは、スケッチを別ホルダーに分けて、Arduinoに!
    スケッチ上設定にresp_cnfld.htmlとありますが、自動で成型します?
    まず、動作確認がしたくて「トンチンカン」な質問ですみません。

    返信削除
  2. すみません。もう一つ質問です。
    設定ファイルは、初めに製作するのですか?
    $cnf_textは、どこに存在しますか?
    色々検索してますが?見つかりません。

    返信削除
  3. 申し訳ありません。レスポンス用のHTML が漏れていました。
    追記しましたので、ご参照下さい。
    $cnf_test は レスポンス用 のHTML にあります。

    尚、各ファイルは
    "スケッチ用フォルダ" ( メインスケッチと同じ名称 )
    に 3つの分割したスケッチと "data" フォルダを格納し、
    "data" フォルダに 各HTML を格納します。

    < スケッチ用フォルダ> --+-- メインスケッチ
                +-- WiFi設定用スケッチ
                +-- LED調光用スケッチ
                +-- --+-- WiFi_Config.html
                +-- main.html
                +-- resp_cnfld.html
    色々、説明不足で、すみません。

    返信削除
  4. とんでもないです。
    webを色々渡り歩いて個々の情報が自分に一番合っていると考えて、勉強をさせていただいています。本当に助かっています。
    まだまだ勉強不足で、質問ばかりですみません。
    (スケッチのコメントやHPが非常に参考になります。)
    respは、前勉強したときにありましたね!自分の物になっていないのがばれますね!
    少しずつですが、頑張って解読しています。
    respは、なくても動いているようで、ここもまだまだ勉強不足です。
    現在、ファイルの事を色々調べながら行っています。
    難しいですね!
    Arduino:rd_spiffsでLODE 
    HTML:scriptでSAVEですよね!
    Arduinoで保存できないのですか?
    すみません。ろくに調べもせず。
    週末にゆっくり調べます。

    返信削除
    返信
    1. SPIFFS への アップロード は Arduino IDE から ESP32 Sketch Data Upload で行いますが、SPIFFS のファイルをダウンロードする ツール が見当たりませんでした。
      また、設定ファイルを ブラウザからダウンロードしたかった為、作成しました。

      resp がなくても動いているようとの事ですが、"設定をダウンロードする"のクリックでダウンロードのウインドウが開いたでしょうか? 正常ならダウンロードの画面が開き、PC (or スマホ) に 設定内容が ダウンロード できます。
      ( レスポンス用HTML が無いと、ダウンロードができないと思います。)

      コードを変えてみたり等して 色々試してみてください。

      削除
  5. 早い回答をありがとうございます。SPIFFSの件了解です。
    PCダウンロードでしたか?動いていません。
    ダイアログも出ていません(^^♪
    他のスケッチ(アップロード&ダウンロード)を動かしたときは、出ています。
    こちらから順番に理解をしようかと思って現在こちらを勉強中です。
    てっきり、ESP32内保存かと?
    そこまで勉強が足していません。
    週末の解読が楽しみになってきました。
    htmlは、空で製作してあるので早速テストします。
    PCダウンロードは、ブラウザからの方がベターなのですね!

    返信削除
    返信
    1. 設定画面から設定した値(ここでは WiFi設定値) は SPIFFS に格納しますが、HTML修正等で HTML をアップロードすると設定値が消えてしまいます。
      この為、設定値をダウンロードしたいと思いました。
      ダウンロードしたファイルを data ディレクトリに置けば、アップロード時に設定値もSPIFFS に格納されます

      削除
  6. 了解しました。動作をして確認してみます。
    いつもすみません。

    返信削除
  7. お久しぶりです。かなり勉強をさせていただきました。
    script少しばかり(ほぼ流用ですが)
    やりたいことが、だいぶできてきました。ありがとうございました。
    そこで、いろいろ調べてたのですが不明な点があり再度質問をさせてください。
    メインの97行から
    if (client) {  //クライアント接続がOK? 接続された? 接続状態?
    while (client.connected()) { // クライアントから接続されたとき
    if (client.available()) { //データがある? //
    と理解していますが、whileの無限ループで、lineに文字列が拾えない状況に
    ごくまれになります。
    ブラウザの自動更新をやっているのでこれが原因か?とも考えています。
    ++ new client! ++表示後、応答がなくなります。
    113行に入らず、breakしない状態みたいです。
    わかりますか?
    他は、いろいろ変更していますが97行以降は触っていないつもりです。
    (理解ができていないため!)
    現在、lineの空でbreakを考えています。問題ないですか?

    返信削除
    返信
    1. ++ new client! ++表示後、応答がなくなるとのことですが、クライアント(ブラウザ) から接続された後、クライアントから何もデータが来ていない(client.available() にならない)様に
      思えます。
      状況がよく判っていませんが、"ブラウザの自動更新"がどのようなもので、ブラウザから何を行ったときに応答しなくなるのでしょうか?
      再現できないとなかなか原因を調べるのは難しいです。

      lineが空で break を考えているとのことですか、 line が空の時は そもそも client.available になっていないと思われますのでやるとすれば、client.available でない場合に break かと思います。

      ただ、通常でも接続後、データが来るまでは client.available でない状態ですので、一定時間 available にならない場合に break する等した方がよいかもしれません。

      削除
  8. 返信ありがとうございます。
    自動更新は、ブラウザで [ http-equiv="refresh" content="60"]
    1分更新してます。この時もlineデータ入ります。
    client.available にならない状態みたいです。
    シリアルモニターでもう少し状況を確認してみます。
    時間での breakも検討してみます。
    全体像が見えないのに、こんな質問をしましてすみませんでした。

    返信削除
    返信
    1. 大分経ってしまい、既に対処済みかもしれませんが、
      client.available が 0 のまま文字列を受信しない場合、Loop を抜けなくなってしまう為、タイムアウト処理追加について投稿しました。「クライアントのタイムアウト」

      削除
  9. すみません。何度も質問ですが?
    wifi設定からのデータ送信は、どこでやっているのですか?
    mainの方は、scriptで行っているのはわかりました。
    GET/?でもらっているのはわかりますが、更新のたびに入っています。
    ????
    質問ばかりですみません。これでもかなり努力をしてお勉強をしています。
    お助け下さ。

    返信削除
    返信
    1. ボタン (type='submit') をクリックした時、
      <form method="get"> ... </form>
      の範囲内の FORM データが GET で送信されます。

      WiFi設定画面では、"送信"ボタンのクリックで設定した値すべて と "set=設定" が GET で送信されます。
      main.html では "ON" ボタンのクリックで "on=ON" が GET で送信されます。
      script ( cnfdl() )で行っているのは "設定をダウンロードする" の文字列をクリックした場合です。

      削除
    2. そうでした!GETが残るのでscriptにかえたのでした。
      中途半端に理解しながら行って入るのですみません。form method="get"を調べようと思って忘れてますた。良く調べます。
      ありがとうございます

      削除
  10. このコメントは投稿者によって削除されました。

    返信削除
  11. お久ぶりです。コロナの影響もあり毎日お勉強ができています。
    やっと自分の行いたい形になってきました。
    以前もコメント書いた覚えがありますが、「パスワードの間違いでAPモード変更したい!」の件ですが、このプログラムにはすでに導入されているのですね!
    モード変更をして、再起動ですね!
    まだテストもできず質問ですが?
    モード変更は、再起動後にい有効ですか?メモリーリセットでは?
    ファイルに保存が必要では?
    パスワードなど設定違いで、再起動したい時など?
    設定ファイルを削除か?起動時に条件を立てるか?
    ブラウザでは、設定できないので??
    現在思案中です。アドバイスがあればお願いします。

    返信削除
  12. このプログラムでは、STAモードで立ち上げたときに WiFiに 10秒以内に接続できない場合、APモードにしてリブートする様にしています。
    が、、、 すみません。設定ファイルにモードを書き戻すのを忘れていました。
    スケッチ (LED調光用) 38,39 行目に設定ファイルの変更を追加しました。
     ( テストしなかったのかな ??? )

    このプログラムではモード変更して設定しても、STA/AP モードが変わらなければ再起動はしていません。(設定の保存のみ)
    変更の反映は手動でリセットした後になります。
    モード変更時にどうするかは、そのプログラム次第と思いますので、一概にどうしたら良いとは言えません。

    返信削除
  13. 適切なアドバイスありがとうございます。
    スケッチの更新もされたのですね!
    よく確認してみます。
    ハード(SW)にて強制的にAPモードに変更を考えてみました。
    仕様上、この方が良いかな?(自分の使い勝手ですが)
    またよく勉強しておきます。

    返信削除