OTA(Over The Air, Wifi経由書き込み)

最終修正日:2020/8/3

【Contents】

  1. スケッチ
    1. 最初はシリアルで書き込む
    2. OTAの書込は2通り
      1. “コンパイルしたバイナリを出力”してダウンロード
      2. ArduinoIDEからターゲットデバイスへ一連の処理をバッチ処理
  2. コラム
    1. Stationモード時のSSIDパスワードの記述
    2. モードの違いによる応答性
※OTA自体はESP32の独自仕様では無く、ArduinoIDEに可成り前から実装されていた仕組みです。
広義として、『無線ネットワークを利用(経由)した、主にデータの受信・同期の際の通信手段を指す語である』
狭義として、『無線LANや携帯電話網を使用して、端末にケーブルをつなぐことなく、ソフトウェアを更新できる技術。』
Arduino IDE 1.6.7で正式サポートしたようです。
OTAの基本的な仕組みはhttps://garretlab.web.fc2.com/arduino/esp32/examples/ArduinoOTA/BasicOTA.htmlを参照してください。

ESP32はプログラムの書き込みをUSB経由以外にWifi経由で実現する事が可能です。これをOTA(Over The Air)といいます。
STM32duinoで、ArduinoIDEから普通にプログラムを書き込む際も、ブートローダ部は維持されユーザプログラム領域を書き換えているわけで、その意味でOTA部というのはブートローダとして書き込まれているので可能なのでしょう。

@ USBシリアル変換/開発ボードであればUSB接続することでビルド後のバイナリファイルをESP32に書き込みます。
A OTAに絡むサンプルコードを正しくBuildすると、接続先にネットワーク接続先が選択出来るようになります。
以後このポートを選択しておくとシリアル接続していなくても、ネットワーク経由でバイナリデータをダウンロード出来るようになります。
B バイナリデータを作成する機能がIDEには用意されています。一旦バイナリファイルを作成し、ブラウザ経由でそのバイナリファイルをダウンロードすることが出来ます。
Aとの違いですが、AはStationModeでないと実現出来ませんが、BはAPModeでも実現出来ます。
ダウンロード速度も納得しうる実行速度です。

先人の記事によると環境に注意する必要があるとあります。Python27が必要でPython3.5では動かないとありました。ところが自分の環境はPython3.6.1(32bit) 最新版は3.8.3とあります。
Pythonを自分でインストールした覚えがありません。とりあえずUpdateすること無くOTAの検証をして見ることにします。

OTAに対応したコードをまずUSB経由で書き込みました。すると、ポートに接続先のネットワークポートが見つかります。そして、そのポートを選択してプログラムを書き込むと、そのIP 宛てにUploadが開始されることが判りました。ただ、今回は成功していません。いろいろ制約があるようです。もう少し検討します。

バイナリファイルを構築後であればブラウザからもUpdate可能となるようです。OTA用のブートローダがダウンロードされたデバイスであればこれが実現するようです。
USB-TTL部を搭載していないモジュールの場合、この機能は有用ですね。

以下はLastMinuteEngineersの提供するサンプルスケッチをレタッチしたモノです。
このプログラムをUSB経由でESP32に書き込みます。

/*
 * 20200709 T.Wanibe
 * LastMinuteEngineersの提供するサンプルコードを元にOTAの勉強をします。
 * https://lastminuteengineers.com/esp32-ota-web-updater-arduino-ide/
 * 最大1310720バイトのフラッシュメモリのうち、スケッチが758818バイト(57%)を使っています。
 * 最大327680バイトのRAMのうち、グローバル変数が41568バイト(12%)を使っていて、ローカル変数で286112バイト使うことができます。
 * 
 */
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
#define HTTPport        80
const char*     host            = "esp32";
const char*     ssid            = "";
const char*     password        = "";
String          bgColor         = "#17A1A5";
WebServer WS(HTTPport);
//Style1
String style1 =
"<style>#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
"input{background:#f1f1f1;border:0;padding:0 15px}body{background:#17A1A5;font-family:sans-serif;font-size:14px;color:#777}"
"#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
"#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#17A1A5;width:0%;height:10px}"
"form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
".btn{background:#17A1A5;color:#fff;cursor:pointer}</style>";
//Style2
String style2 =
"<style>#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
"input{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}"
"#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
"#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}"
"form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
".btn{background:#3498db;color:#fff;cursor:pointer}</style>";
//Login page
String loginIndex = 
"<form name=loginForm>"
        "<h1>ESP32 Login</h1>"
"<input name=userid placeholder='User ID'> "
"<input name=pwd placeholder=Password type=Password> "
"<input type=submit onclick=check(this.form) class=btn value=Login></form>"
"<script>"
"function check(form) {"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{window.open('/serverIndex')}"
"else"
"{alert('Error Password or Username')}"
"}"
"</script>" + style1;
//Server Index Page
String serverIndex = 
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update' id='file' onchange='sub(this)' style=display:none>"
"<label id='file-input' for='file'>   Choose file...</label>"
"<input type='submit' class=btn value='Update'>"
"<br><br>"
"<div id='prg'></div>"
"<br><div id='prgbar'><div id='bar'></div></div><br></form>"
"<script>"
"function sub(obj){"
"var fileName = obj.value.split('\\\\');"
"document.getElementById('file-input').innerHTML = '   '+ fileName[fileName.length-1];"
"};"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
"$.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"$('#bar').css('width',Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!') "
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>" + style2;
//------------setup function
void setup(void) {
        Serial.begin(115200);
        // Connect to WiFi network
        WiFi.begin(ssid, password);
        Serial.println(F(""));
        // Wait for connection
        while (WiFi.status() != WL_CONNECTED) {
                delay(500);
                Serial.print(F("."));
        }
        Serial.println(F(""));Serial.print(F("Connected to "));
        Serial.println(ssid);
        Serial.print(F("IP address: "));Serial.println(WiFi.localIP());
        /*use mdns for host name resolution*/
        if (!MDNS.begin(host)) {                                //https://esp32.local
                Serial.println(F("Error setting up MDNS responder!"));
                while (1) {
                        delay(1000);
                }
        }
        Serial.println(F("mDNS responder started"));
        /*return index page which is stored in serverIndex */
        WS.on("/", HTTP_GET, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/html", loginIndex);
        });
        WS.on("/serverIndex", HTTP_GET, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/html", serverIndex);
        });
        /*handling uploading firmware file */
        WS.on("/update", HTTP_POST, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
                ESP.restart();
        }, []() {
                HTTPUpload& upload = WS.upload();
                if (upload.status == UPLOAD_FILE_START) {
                        Serial.printf("Update: %s\n", upload.filename.c_str());
                        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {               //start with max available size
                                Update.printError(Serial);
                        }
                } else if (upload.status == UPLOAD_FILE_WRITE) {
                        /* flashing firmware to ESP*/
                        if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
                                Update.printError(Serial);
                        }
                } else if (upload.status == UPLOAD_FILE_END) {
                        if (Update.end(true)) { //true to set the size to the current progress
                                Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
                        } else {
                                Update.printError(Serial);
                        }
                }
        });
        WS.begin();
}
//------------
void loop(void) {
        WS.handleClient();
        delay(1);
}

シリアルモニタを開いて実行すると、起動時に接続したネットワークからIPアドレスが割り振られ、表示されます。
そのIPアドレスにブラウザからアクセスするとログイン画面が現れます。これで前段が確認出来た事になります。
※DNSも起動しており、https://esp32.localでもログイン画面が確認出来るはずです。
ログインパスワードはHTMLに書き込まれていますが、ユーザーID:“admin” パスワード:“admin”となっています。

serverIndexのページに飛びました。ここで.bin形式のファイルを選択して、Uploadすると、プログラムがweb経由で書き込まれ、リセットされて新たなプログラムが実行されることになります。

この仕組みを繰り返し使う為にUploadするbin形式のファイルにはOTAの機能が組み込まれている必要があります。テンプレートファイルを検討しました。以下の通りです。
実際にレタッチ後、ArduinoIDEでbinファイルを作成する必要があります。
[ スケッチ] > [ コンパイルしたバイナリを出力]に移動します。binファイルはinoファイルと同階層に作成されているはずです

binファイルを“Update”したところ、ESP32は勝手にリスタートしてアクセスポイントに接続しました。次のプログラムは更新に使用したプログラムですが、13pinに接続したLEDの点滅を追加したところ、ダウンロード後に勝手にリセットされ点滅開始します。
書き込み時間は先人が言うように短いかもしれません。ただ、事前にコンパイル済みですのでその時間を考慮する必要があります。
ブラウザからのダウンロードはシリアルコンソールを開いたままでも出来るためデバッグがしやすいです。

/*
 * 20200709 T.Wanibe
 * 最大1310720バイトのフラッシュメモリのうち、スケッチが758874バイト(57%)を使っています。
 * 最大327680バイトのRAMのうち、グローバル変数が41576バイト(12%)を使っていて、ローカル変数で286104バイト使うことができます。
 */
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
#define         HTTPport        80
const char*     host            = "esp32";
const char*     ssid            = "";
const char*     password        = "";
//variabls for blinking an LED with Millis
const int       led             = 13;                           //オンボードLEDが接続されているESP32ピン
unsigned long   previousMillis  = 0;                            //LEDが最後に更新されたときに保存されます
const long      interval        = 1000;                         //点滅する間隔(ミリ秒)
int             ledState        = LOW;                          //LEDの設定に使用されるledState
WebServer WS(HTTPport);
/* Style */
String style =
"<style>"
        "#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
        "input{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}"
        "#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
        "#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}"
        "form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
        ".btn{background:#3498db;color:#fff;cursor:pointer}"
"</style>";
/* Login page */
String loginIndex = 
"<form name=loginForm>"
        "<h1>ESP32 Login</h1>"
        "<input name=userid placeholder='User ID'> "
        "<input name=pwd placeholder=Password type=Password> "
"<input type=submit onclick=check(this.form) class=btn value=Login></form>"
"<script>"
        "function check(form) {"
                "if(form.userid.value=='admin' && form.pwd.value=='admin')"
                        "{window.open('/serverIndex')}"
                "else"
                        "{alert('Error Password or Username')}"
        "}"
"</script>" + style;
/* Server Index Page */
String serverIndex = 
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
        "<input type='file' name='update' id='file' onchange='sub(this)' style=display:none>"
        "<label id='file-input' for='file'>   Choose file...</label>"
        "<input type='submit' class=btn value='Update'>"
        "<br><br>"
        "<div id='prg'></div>"
"<br><div id='prgbar'><div id='bar'></div></div><br></form>"
"<script>"
        "function sub(obj){"
        "var fileName = obj.value.split('\\\\');"
        "document.getElementById('file-input').innerHTML = '   '+ fileName[fileName.length-1];"
        "};"
        "$('form').submit(function(e){"
        "e.preventDefault();"
        "var form = $('#upload_form')[0];"
        "var data = new FormData(form);"
        "$.ajax({"
        "url: '/update',"
        "type: 'POST',"
        "data: data,"
        "contentType: false,"
        "processData:false,"
        "xhr: function() {"
                "var xhr = new window.XMLHttpRequest();"
                "xhr.upload.addEventListener('progress', function(evt) {"
                        "if (evt.lengthComputable) {"
                                "var per = evt.loaded / evt.total;"
                                "$('#prg').html('progress: ' + Math.round(per*100) + '%');"
                                "$('#bar').css('width',Math.round(per*100) + '%');"
                        "}"
                "}, false);"
                "return xhr;"
        "},"
        "success:function(d, s) {"
                "console.log('success!') "
        "},"
        "error: function (a, b, c) {"
        "}"
        "});"
        "});"
"</script>" + style;
//------------ setup function
void setup(void) {
        pinMode(led,  OUTPUT);
        Serial.begin(115200);
        // Connect to WiFi network
        WiFi.begin(ssid, password);
        // Wait for connection
        Serial.println(F(""));
        while (WiFi.status() != WL_CONNECTED) {
                delay(500);
                Serial.print(F("."));
        }
        Serial.println(F(""));
        Serial.print(F("Connected to "));Serial.println(ssid);
        Serial.print(F("IP address: "));Serial.println(WiFi.localIP());
        //ホスト名解決にmdnsを使用する
        if (!MDNS.begin(host)) {                                //https://esp32.local
                Serial.println(F("Error setting up MDNS responder!"));
                while (1) {
                        delay(1000);
                }
        }
        Serial.println(F("mDNS responder started"));
        //serverIndexに格納されているインデックスページを返す
        WS.on("/", HTTP_GET, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/html", loginIndex);
        });
        WS.on("/serverIndex", HTTP_GET, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/html", serverIndex);
        });
        //ファームウェアファイルのアップロードの処理
        WS.on("/update", HTTP_POST, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
                ESP.restart();
        }, []() {
                HTTPUpload& upload = WS.upload();
                if (upload.status == UPLOAD_FILE_START) {
                        Serial.printf("Update: %s\n", upload.filename.c_str());
                        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {       //利用可能な最大サイズから始める
                                Update.printError(Serial);
                        }
                } else if (upload.status == UPLOAD_FILE_WRITE) {
                        //ESPへのファームウェアのフラッシュ
                        if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
                                Update.printError(Serial);
                        }
                } else if (upload.status == UPLOAD_FILE_END) {
                        if (Update.end(true)) {                         //サイズを現在の進行状況に設定する場合はtrue
                                Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
                        } else {
                                Update.printError(Serial);
                        }
                }
        });
        WS.begin();
}
//------------
void loop(void) {
        WS.handleClient();
        delay(1);
        //ループして遅延なく点滅する
        unsigned long currentMillis = millis();
        if (currentMillis - previousMillis >= interval) {
                //最後にLEDを点滅させた時間を保存します
                previousMillis = currentMillis;
                //LEDがオフの場合はオンにし、逆の場合も同様です。
                ledState = not(ledState);
                //変数のledStateでLEDを設定します。
                digitalWrite(led,  ledState);
        }
}

もう一つOLCDを追加したコードを書いてみました。アクセスすべきIPアドレスやSSID/PASSを表示するために使用しています。
また、modeを[WIFI_AP_STA]にしています。アクセスするIPが2通りになります。
I2CのBMP280をアクセスしてデータ表示するようにしています。BME280I2C.hを採用しています。なぜかAdafruitのライブラリがうまくコンパイル出来なかったからです。

コンパイルするために以下のライブラリを追加してください。尚、arduino_secrets.hというのはメインソースに記述したくないLocalな値を別ファイル化する仕組みです。

/*
 * 20200731 T.Wanibe
 * LEDの配置等を変更
 * OTAを維持したまま、モードをST+APにして普段はPCとの間でトランシーバモード、一方、ファーム更新はSTモードで実現出来ないか確認する
 * Adafruit_BME280.hがESP32でうまく動作しないため、ライブラリを変更し、BME280I2C.hを採用した
 * ※HTMLの記述にてString型を駆使して記述するが、その手法も多岐にわたる。好みの方法を確立しておく必要がある
 * 最大1310720バイトのフラッシュメモリのうち、スケッチが840414バイト(64%)を使っています。
 * 最大327680バイトのRAMのうち、グローバル変数が43072バイト(13%)を使っていて、ローカル変数で284608バイト使うことができます。
 */
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
//#include <Adafruit_Sensor.h>
//#include <Adafruit_BME280.h>
#include <BME280I2C.h>
//#include "index.h"                                                      //Web page header file
#include "arduino_secrets.h"                                            //機密データを「Secret」タブ/arduino_secrets.hに入力してください
#define HTTPport        80
#define SCREEN_WIDTH    128                                             // OLED display width, in pixels
#define SCREEN_HEIGHT   64                                              // OLED display height, in pixels
#define OLED_RESET      -1                                              // Reset pin # (or -1 if sharing Arduino reset pin)
#define NUMFLAKES       10                                              // アニメーション例の雪片の数
#define LOGO_HEIGHT     16
#define LOGO_WIDTH      16
#define OLCDAres        0x3C                                            //0x3Cか0x3D
#define LED             13                                              //ボード上のLEDは使えないのでGPIO13にLEDを接続
#define AIport          A0
#define PO1             17
#define PO2             16
#define PO3             4
#define PO4             2
#define PI1             39
#define PI2             34
#define PI3             35
#define PI4             33
//#define BMEID           0x58
const char*     host            = "esp32";
char            ssid[]          = SECRET_SSID;                  //ネットワークSSID(名前)
char            pass[]          = SECRET_PASS;                  //ネットワークパスワード(WPAの使用、またはWEPのキーとして使用)
String          myID            = "admin";
String          myPASS          = "password";
//variabls for blinking an LED with Millis
unsigned long   previousMillis  = 0;                            //LEDが最後に更新されたときに保存されます
const long      interval        = 1000;                         //点滅する間隔(ミリ秒)
int             ledState        = LOW;                          //LEDの設定に使用されるledState
unsigned        status;
//Station用
IPAddress       ip(192, 168, 0, 33);
IPAddress       gateway(192, 168, 0, 1);
IPAddress       netmask(255, 255, 255, 0);
//SoftAP用
const char*     ap_ssid         = "ESP_AP";
const char*     ap_password     = "password";
char            buf1[128];
char            buf2[128];
String          buf11           = "ABC";
String          buf22           = "DEF";
float           temp(NAN),hum(NAN),pres(NAN);                   // hum(NAN)は無効

IPAddress       ap_ip(192, 168, 250, 33);
IPAddress       ap_gateway(192, 168, 250, 1);
WebServer       WS(HTTPport);
//I2C(SDA、SCLピン)に接続されたSSD1306ディスプレイの宣言
Adafruit_SSD1306 OLCD(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
BME280I2C       bmp;                                            // Default : forced mode, standby time = 1000 ms
                                                                // Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off,
//-------------- Style
String style =
"<style>\n"
        "\t#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}\n"
        "\tinput{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}\n"
        "\t#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}\n"
        "\t#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}\n"
        "\tform{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}\n"
        "\t.btn{background:#3498db;color:#fff;cursor:pointer}\n"
"</style>\n";
//-------------- Login page
String loginIndex = 
"<form name=loginForm>\n"
        "\t<h1>ESP32 Login</h1>\n"
        "\t<input name=userid placeholder='User ID'>\n"
        "\t<input name=pwd placeholder=Password type=Password>\n"
"<input type=submit onclick=check(this.form) class=btn value=Login>\n</form>\n"
"<script>\n"
        "\tfunction check(form) {\n"
                "\t\tif(form.userid.value== '" + myID + "' && form.pwd.value== '" + myPASS +"' )\n"
                        "\t\t\t{window.open('/serverIndex')}\n"
                "\t\telse\n"
                        "\t\t\t{alert('Error Password or Username')}\n"
        "\t}\n"
"</script>\n" + style;
//-------------- Server Index Page
String serverIndex = 
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>\n"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>\n"
        "\t<input type='file' name='update' id='file' onchange='sub(this)' style=display:none>\n"
        "\t<label id='file-input' for='file'>   Choose file...</label>\n"
        "\t<input type='submit' class=btn value='Update'>\n"
        "\t<br><br>\n"
        "\t<div id='prg'></div>\n"
        "\t<br><div id='prgbar'><div id='bar'></div></div><br>\n"
"</form>\n"
"<script>\n"
        "\tfunction sub(obj){\n"
                "\t\tvar fileName = obj.value.split('\\\\');\n"
                "\t\tdocument.getElementById('file-input').innerHTML = '   '+ fileName[fileName.length-1];\n"
        "\t};\n"
        "\t$('form').submit(function(e){\n"
                "\t\te.preventDefault();\n"
                "\t\tvar form = $('#upload_form')[0];\n"
                "\t\tvar data = new FormData(form);\n"
                "\t\t$.ajax({\n"
                        "\t\t\turl: '/update',\n"
                        "\t\t\ttype: 'POST',\n"
                        "\t\t\tdata: data,\n"
                        "\t\t\tcontentType: false,\n"
                        "\t\t\tprocessData:false,\n"
                        "\t\t\txhr: function() {\n"
                                "\t\t\t\tvar xhr = new window.XMLHttpRequest();\n"
                                "\t\t\t\txhr.upload.addEventListener('progress', function(evt) {\n"
                                        "\t\t\t\t\tif (evt.lengthComputable) {\n"
                                                "\t\t\t\t\t\tvar per = evt.loaded / evt.total;\n"
                                                "\t\t\t\t\t\t$('#prg').html('progress: ' + Math.round(per*100) + '%');\n"
                                                "\t\t\t\t\t\t$('#bar').css('width',Math.round(per*100) + '%');\n"
                                        "\t\t\t\t\t}\n"
                                "\t\t\t\t}, false);\n"
                                "\t\t\t\treturn xhr;\n"
                        "\t\t\t},\n"
                        "\t\t\tsuccess:function(d, s) {\n"
                                "\t\t\t\tconsole.log('success!')\n"
                        "\t\t\t},\n"
                        "\t\t\terror: function (a, b, c) {\n"
                        "\t\t\t}\n"
                "\t\t});\n"
        "\t});\n"
"</script>\n" + style;
//------------ viewValue
String viewValue = 
"<html>\n"
        "\t<head>\n"
        "\t<meta http-equiv='refresh' content='5'/>\n"
        "\t<title>ESP32 Monitor</title>\n"
        "\t<style>\n"
                "\t\tbody { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\n"
        "\t</style>\n"
        "\t</head>\n"
        "\t<body>\n"
                "\t\t<center><h1>ESP32 Monitor!</h1>\n"
                "\t\t<p>" + buf11 + "</p>\n"
                "\t\t<p>" + buf22 + "</p></center>\n"
        "\t</body>\n"
"</html>\n";
//------------ fileNotFound
String fileNotFound = 
"<html><head>\n"
        "\t<title>ESP32 : 404 Not Found</title>\n"
"</head><body>\n"
        "\t<center><h1>ESP32 404 : Not Found</h1></center>\n"
        "\t<p>The requested URL was not found on this server.</p>\n"
"</body></html>\n";
//------------This routine is executed when you open its IP in browser
//void handleRoot() {
//        String s        = MAIN_page;                            //Read HTML contents
//        WS.send(200, "text/html", s);                           //Send web page
//}
//------------
void handleGetData() {
        //float a           = analogRead(A0);
        //String adcValue = String(a);
        WS.send(200, "text/html", SendHTML(analogRead(A0),temp,pres));       //Send ADC value only to client ajax request
}
//------------
void handleNotFound(){
        //WS.send(404, "text/plain", "Not found");
        WS.send(404, "text/html",fileNotFound);
}
//------------
String SendHTML(float tempSensor1,float tempSensor2,float tempSensor3){
        String ptr =
        "<html>\n"
        "<head><meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no' charset='UTF-8'>\n"
                "\t<title>ESP32 Monitor</title>\n"
                "\t<style>\n\t\thtml { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n"
                        "\t\tbody{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;}\n"
                        "\t\tp {font-size: 24px;color: #444444;margin-bottom: 10px;}\n"
                "\t</style>\n"
        "</head>\n"
        "<body>\n"
                "\t<div id=\"webpage\">\n"
                "\t<h1>ESP32 Monitor</h1>\n"
                "\t<p>AD0 AD値: " + String(tempSensor1) + "</p>\n"
                "\t<p>Temp: "     + String(tempSensor2) + "℃</p>\n"
                "\t<p>Pres: "     + String(tempSensor3) + "Pa</p>\n"
                "\t</div>\n"
        "</body>\n"
        "</html>\n";
        return ptr;
}
//------------ BMPprint
void printValues() {
        BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
        BME280::PresUnit presUnit(BME280::PresUnit_Pa);
        bmp.read(pres, temp, hum, tempUnit, presUnit);
        sprintf(buf1,"Temperature:%.2f%1s",temp,String(tempUnit == BME280::TempUnit_Celsius ? 'C' :'F'));
        sprintf(buf2,"Pressure: %.0f Pa",pres); 
        buf11   = String(buf1);
        buf22   = String(buf2);
        Serial.println(buf1);
        Serial.println(buf2);
        OLCD.setCursor(0, 40);
        OLCD.fillRect(0,40,128,24,SSD1306_BLACK);
        OLCD.setTextSize(1);OLCD.setTextColor(SSD1306_WHITE);OLCD.setCursor(0, 40);
        OLCD.println(buf1);
        OLCD.println(buf2);
        OLCD.display();
}
//------------ setup function
void setup(void) {
        pinMode(LED,OUTPUT);    digitalWrite(LED,LOW);                  //Onboard LED port Direction output
        pinMode(PO1,OUTPUT);     digitalWrite(PO1,LOW);
        pinMode(PO2,OUTPUT);     digitalWrite(PO2,LOW);
        pinMode(PO3,OUTPUT);     digitalWrite(PO3,LOW);
        pinMode(PO4,OUTPUT);     digitalWrite(PO4,LOW);
        pinMode(PI1,INPUT);
        pinMode(PI2,INPUT);
        pinMode(PI3,INPUT);
        pinMode(PI4,INPUT);
        //
        Serial.begin(115200);
        //SSD1306_SWITCHCAPVCC =内部で3.3Vから表示電圧を生成
        Wire.setClock(400000);                                          // クロック速度を最も速くなるように設定して、通信を改善します(高速モード)
        if(!OLCD.begin(SSD1306_SWITCHCAPVCC, OLCDAres)) {               // 128x64のスレーブアドレスオリジナル値は0x3Dでしたが手元の元は0x3Cでした。
                Serial.println(F("SSD1306 allocation failed"));
                for(;;);                                                // Don't proceed, loop forever
        }
        status = bmp.begin();
        if (!status) {
                Serial.println(F("Could not find BME280 sensor!"));
        }
        // bme.chipID(); // Deprecated. See chipModel().
        switch(bmp.chipModel()){
                case BME280::ChipModel_BME280:
                        Serial.println(F("Found BME280 sensor! Success."));
                        break;
                case BME280::ChipModel_BMP280:
                        Serial.println(F("Found BMP280 sensor! No Humidity available."));
                        break;
                default:
                        Serial.println(F("Found UNKNOWN sensor! Error!"));
        }
        // バッファをクリアします
        OLCD.clearDisplay();
        // アクセスポイント(無線LAN親機) + ステーション(無線LAN子機)
        WiFi.mode(WIFI_AP_STA);
        // まず既存のアクセスポイント(ネットワーク)に接続する
        //WiFi.config(ip,gateway,netmask);                              //固定IPにする場合に使用
        WiFi.begin(ssid, pass);
        // Wait for connection
        Serial.println(F(""));
        while (WiFi.status() != WL_CONNECTED) {                         //接続するまでループするが、すごく電流を喰うようでchipが熱くなる。要対策。
                delay(1000);
                Serial.print(F("."));
        }
        Serial.println(F(""));
        Serial.print(F("Connected to "));Serial.println(ssid);
        IPAddress myIP =WiFi.localIP();
        Serial.print(F("IP address: "));Serial.println(myIP);
        OLCD.setTextSize(1);OLCD.setTextColor(SSD1306_WHITE);OLCD.setCursor(0, 0);
        OLCD.print(F("LoginID:"));OLCD.println(myID);
        OLCD.print(F("LoginPASS:"));OLCD.println(myPASS);
        OLCD.print(F("IP:"));OLCD.println(myIP);
        //OLCD.display();
        // SoftAPを開始する
        WiFi.softAPConfig(ap_ip,ap_gateway,netmask);
        WiFi.softAP(ap_ssid, ap_password);
        //IPAddress ap_ip = WiFi.softAPIP();
        OLCD.setTextSize(1);OLCD.setTextColor(SSD1306_WHITE);OLCD.setCursor(0, 40);
        OLCD.print(F("SSID:"));OLCD.println(ap_ssid);
        OLCD.print(F("PASS:"));OLCD.println(ap_password);
        OLCD.print(F("IP:"));OLCD.println(ap_ip);
        OLCD.display();
        Serial.print(F("SoftAPのIPアドレス(LAN側): "));Serial.println(myIP);
        //ホスト名解決にmdnsを使用する
        if (!MDNS.begin(host)) {                                //https://esp32.local
                Serial.println(F("Error setting up MDNS responder!"));
                OLCD.setCursor(0, 32);OLCD.print(F("Error setting up MDNS responder!"));
                while (1) {
                        delay(1000);
                }
        }
        Serial.println(F("mDNS responder started"));
        //serverIndexに格納されているインデックスページを返す
        WS.on("/login", HTTP_GET, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/html", loginIndex);
                OLCD.fillRect(0,32,128,8,SSD1306_BLACK);
                OLCD.setCursor(0, 32);
                OLCD.setTextColor(SSD1306_WHITE);
                OLCD.print(F("Connection"));
                OLCD.display();
        });
        WS.on("/serverIndex", HTTP_GET, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/html", serverIndex);
                OLCD.fillRect(0,32,128,8,SSD1306_BLACK);
                OLCD.setCursor(0, 32);
                OLCD.setTextColor(SSD1306_WHITE);
                OLCD.print(F("UpdateReady"));
                OLCD.display();
        });
        WS.on("/", HTTP_GET, []() {
                WS.sendHeader("Connection", "close");
                WS.send(200, "text/html", SendHTML(analogRead(A0),temp,pres));
                //OLCD.setCursor(0, 32);
                OLCD.fillRect(0,32,128,8,SSD1306_BLACK);
               OLCD.display();
        });
        //ファームウェアファイルのアップロードの処理
        WS.on("/update", HTTP_POST, []() {
                WS.sendHeader("Connection", "close");
                OLCD.setCursor(0, 32);
                OLCD.drawRect(0,32,128,8,SSD1306_BLACK);
                OLCD.display();
                WS.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
                ESP.restart();
        }, []() {
                HTTPUpload& upload = WS.upload();
                if (upload.status == UPLOAD_FILE_START) {
                        Serial.printf("Update: %s\n", upload.filename.c_str());
                        OLCD.setCursor(0, 32);
                        OLCD.drawRect(0,32,128,8,SSD1306_BLACK);
                        OLCD.setTextColor(SSD1306_WHITE);
                        OLCD.print(F("Updating..."));
                        OLCD.display();
                        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {       //利用可能な最大サイズから始める
                                Update.printError(Serial);
                        }
                } else if (upload.status == UPLOAD_FILE_WRITE) {
                        //ESPへのファームウェアのフラッシュ
                        if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
                                Update.printError(Serial);
                        }
                } else if (upload.status == UPLOAD_FILE_END) {
                        if (Update.end(true)) {                         //サイズを現在の進行状況に設定する場合はtrue
                                Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
                        } else {
                                Update.printError(Serial);
                        }
                }
        });
        //WS.on("/", handleRoot);                                       //This is display page
        //WS.on("/", handleGetData);                                      //To get update of ADC Value only
        WS.on("/GetData", handleGetData);                           //To get update of ADC Value only
        //WS.on("/readPres", handlePres);                               //To get update of ADC Value only
        //WS.on("/readTemp", handleTemp);                               //To get update of ADC Value only
        WS.onNotFound(handleNotFound);
        WS.begin();
}
//------------
int count = 0;
void loop(void) {
        WS.handleClient();
        delay(100);
        printValues();
        //ループして遅延なく点滅する
        unsigned long currentMillis = millis();
        if (currentMillis - previousMillis >= interval) {
                //最後にLEDを点滅させた時間を保存します
                previousMillis = currentMillis;
                //LEDがオフの場合はオンにし、逆の場合も同様です。
                ledState = not(ledState);
                //変数のledStateでLEDを設定します。
                digitalWrite(LED, ledState);
                switch(count){
                        case 0:
                                digitalWrite(PO1, true);
                                digitalWrite(PO2, false);
                                digitalWrite(PO3, false);
                                digitalWrite(PO4, false);
                                break;
                        case 1:
                                digitalWrite(PO1, false);
                                digitalWrite(PO2, true);
                                digitalWrite(PO3, false);
                                digitalWrite(PO4, false);
                                break;
                        case 2:
                                digitalWrite(PO1, false);
                                digitalWrite(PO2, false);
                                digitalWrite(PO3, true);
                                digitalWrite(PO4, false);
                                break;
                        case 3:
                                digitalWrite(PO1, false);
                                digitalWrite(PO2, false);
                                digitalWrite(PO3, false);
                                digitalWrite(PO4, true);
                                break;
                        default:
                                digitalWrite(PO1, false);
                                digitalWrite(PO2, false);
                                digitalWrite(PO3, false);
                                digitalWrite(PO4, false);
                                break;
                }
                count = (count + 1) % 5;
                Serial.println(count);
        }
}

Stationモード時のSSIDパスワードの記述】

ESP32のスケッチ記述でStationモードで使用する場合、ソースコード内に接続するアクセスポイントのSSIDとパスワードを記述することになります。このままでは公開できません。
そんなことを思っていたら記述方法が紹介されていました。備忘録とします。

  • ソースコードに隠したい情報は別ファイルに記述し、そのファイルをインクルードすることで解決します。
  • SSIDとパスワードを隠したい場合、この2つの情報を記述したファイルを作成して、ファイル名を“arduino_secrets.h”として.inoと同階層に保存します。
  • “arduino_secrets.h”の内容は、以下のように記述します。
    #define SECRET_SSID "SSID"
    #define SECRET_PASS "PASS"
  • .inoには次のような記述を追加します。
    #include "arduino_secrets.h" 
    ・・・・
    char ssid[]  = SECRET_SSID;                  //
    char pass[]  = SECRET_PASS;                  //
    ・・・・
    WiFi.mode(WIFI_STA);                         //WIFI_AP_STAの場合も同様
    WiFi.begin(ssid, pass);


OTAを実行する上で3つあるMODE(WIFI_STA/WIFI_AP/WIFI_AP_STA) いずれでも実現可能であることを確認しました。WIFI_AP_STAの場合、2つのポイントからアクセスする事になりますが、OTAのサーバループは共通で使える様です。

WIFI_APモードでも使えることから、無線LANを装備したNotePCとESPモジュール間でトランシーバライクに使えることになります。ESP32側は電源だけ確保出来ればESP-WROOM32単体で機能することになり、ロボット組込も優位になりますね。

ちなみにAPモードでOTAを実施すると消費電流値が40mA程度上昇しました。通常実行時に0.15〜0.17Aだったのが、0.18〜0.21Aになりました。

ロボット等のコントローラにESP32を採用する際、OTAの仕組みは有り難いです。最初だけはシリアル接続してBOOTボタンを細工してプログラムを書き込むわけですが、それ以降はブラウザからダウンロード出来ます。

モードによってバイナリファイルのダウンロード時間も変わるようです。APモードは一番効率が良さそうです。

モードの違いによる応答性】

ESP32のスケッチ記述でbegin()実行前にmode()を設定し、ステーションモード/アクセスポイントモードが設定出来ます。
ブラウザで表示するようなアプリケーションの場合、どちらの方法を使うと反応がいいのかということを気にしました。
アクセスモードではPCとESP32が直接通信することになります。
一方ステーションモードではPCとESP32の間に無線LAN接続ルータを介して接続することになります。
経路長からするとアクセスモードの方が効率的だと思われますが、確認してみたいと思いました。

方法論としてはpingによる応答性で確認すれば良いかと思います。以下は実際の結果です。

Stationモード時

192.168.0.17 に ping を送信しています 32 バイトのデータ:
192.168.0.17 からの応答: バイト数 =32 時間 =78ms TTL=255
192.168.0.17 からの応答: バイト数 =32 時間 =103ms TTL=255
192.168.0.17 からの応答: バイト数 =32 時間 =19ms TTL=255
192.168.0.17 からの応答: バイト数 =32 時間 =43ms TTL=255
192.168.0.17 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 19ms、最大 = 103ms、平均 = 60ms

アクセスポイントモード時

192.168.250.33 に ping を送信しています 32 バイトのデータ:
192.168.250.33 からの応答: バイト数 =32 時間 =9ms TTL=255
192.168.250.33 からの応答: バイト数 =32 時間 =2ms TTL=255
192.168.250.33 からの応答: バイト数 =32 時間 =2ms TTL=255
192.168.250.33 からの応答: バイト数 =32 時間 =2ms TTL=255
192.168.250.33 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 2ms、最大 = 9ms、平均 = 3ms

PC側の無線LANアダプタは安価なEDUPのEP-N8508GSです。無線LANステーションはTP-LINKのArcher AX10 です。
明らかにESP32のアクセスモードでの接続が応答性がいいことが判りました。
OTAによるファームのアップデートもアクセスモードの方が反応がいいように感じます。


戯言(nonsense)に戻る


問い合わせ頁の表示


免責事項

本ソフトウエアは、あなたに対して何も保証しません。本ソフトウエアの関係者(他の利用者も含む)は、あなたに対して一切責任を負いません。
あなたが、本ソフトウエアを利用(コンパイル後の再利用など全てを含む)する場合は、自己責任で行う必要があります。

本ソフトウエアの著作権はToolsBoxに帰属します。
本ソフトウエアをご利用の結果生じた損害について、ToolsBoxは一切責任を負いません。
ToolsBoxはコンテンツとして提供する全ての文章、画像等について、内容の合法性・正確性・安全性等、において最善の注意をし、作成していますが、保証するものではありません。
ToolsBoxはリンクをしている外部サイトについては、何ら保証しません。
ToolsBoxは事前の予告無く、本ソフトウエアの開発・提供を中止する可能性があります。

商標・登録商標

Microsoft、Windows、WindowsNTは米国Microsoft Corporationの米国およびその他の国における登録商標です。
Windows Vista、Windows XPは、米国Microsoft Corporation.の商品名称です。
LabVIEW、National Instruments、NI、ni.comはNational Instrumentsの登録商標です。
I2Cは、NXP Semiconductors社の登録商標です。
その他の企業名ならびに製品名は、それぞれの会社の商標もしくは登録商標です。
すべての商標および登録商標は、それぞれの所有者に帰属します。