M5StickC(ESP32)のOTAでのプログラム更新

現時点の情報です。最新情報はM5StickC非公式日本語リファレンスを確認してみてください。

ESP32のOTA方式で、更に検証しています。

概要

M5StickC(ESP32)をWi-Fi経由でプログラムをOTA(Over The Air)で更新する方法を調べてみました。

OTAとは?

主に無線を使ってプログラムを更新する方法です。とはいえ、有線LANを使っても同じことが可能ですので、シリアル接続や書き込み専用ケーブルなどを利用しない更新方法のことです。

ESP32のプログラム更新方法

シリアル接続

一番一般的な方法で、USBシリアルを利用して書き込みを行う方式です。パソコンと物理的に接続しますので、比較的安定して書き込みをすることができます。

JTAG(ESP32では更新不可)

M5StickCは端子がないので無理ですが、ESP32ではデバッグで利用するJTAG接続もできます。しかしながらESP32のJTAGだとフラッシュへの書き込みができませんので、プログラムの更新には利用できません。

BASIC OTA(特定ポートでの待受)

ESP32側の特定ポートでOTA受信処理を行うモードです。パソコン側からESP32のOTA待受ポートに対して、BINファイルを送信して更新を行います。

Arduino IDEからプログラムの送信を行うこともできる、標準的なOTAの方式です。インタネット経由ではなく、ローカルネットワークからの更新が前提となります。

OTAWebUpdater(Web経由での更新)

ESP32にブラウザでアクセスを行い、BINファイルをアップロードすることで、プログラム更新を行います。

Nefry BTで採用されていました。ブラウザから操作する画面があるのであれば、この方式も便利だと思います。

サンプルはブラウザからアップロードしていますが、WebサーバーからESP32にダウンロードする方向でも、更新は可能だと思われます。

サンプルスケッチ(BasicOTA)

#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <M5StickC.h>
const char* ssid = "..........";
const char* password = "..........";
void setup() {
  M5.begin();
  M5.Lcd.fillScreen(GREEN);
  M5.Lcd.print("Hello World");
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  // Port defaults to 3232
  // ArduinoOTA.setPort(3232);
  // Hostname defaults to esp3232-[MAC]
  // ArduinoOTA.setHostname("myesp32");
  // No authentication by default
  // ArduinoOTA.setPassword("admin");
  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
  ArduinoOTA
  .onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH)
      type = "sketch";
    else // U_SPIFFS
      type = "filesystem";
    // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
    Serial.println("Start updating " + type);
  })
  .onEnd([]() {
    Serial.println("\nEnd");
  })
  .onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  })
  .onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}
void loop() {
  ArduinoOTA.handle();
}

スケッチ例のssidとpasswordだけ書き換えることで動かせます。上記はM5StickC用に画面初期化だけ追加してあります。

初回はシリアル経由で送信し、その後はシリアル選択の場所にESP32のIPアドレスが追加されますので、それを選択することでWi-Fi経由で更新が可能です。画面の初期化色を変更するなどして、プログラムが更新されたかを確認してください。

ただし、Windowsの場合にはファイヤーウォールの設定変更が必要になったり、Wi-Fi環境によっては安定動作しなかったりします。

デフォルトでは認証無しに更新していますので、実運用ではパスワード認証をする必要があります。

ポート番号は3232がデフォルトで、変更も可能ですが変更するとArduino IDEからは転送できなくなります。espota.exeやespota.pyを利用してコマンドラインから更新することはできます。

サンプルスケッチ(OTAWebUpdater)

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
#include <M5StickC.h>
const char* host = "esp32";
const char* ssid = "xxx";
const char* password = "xxxx";
WebServer server(80);
/*
 * Login page
 */
const char* loginIndex = 
 "<form name='loginForm'>"
    "<table width='20%' bgcolor='A09F9F' align='center'>"
        "<tr>"
            "<td colspan=2>"
                "<center><font size=4><b>ESP32 Login Page</b></font></center>"
                "<br>"
            "</td>"
            "<br>"
            "<br>"
        "</tr>"
        "<td>Username:</td>"
        "<td><input type='text' size=25 name='userid'><br></td>"
        "</tr>"
        "<br>"
        "<br>"
        "<tr>"
            "<td>Password:</td>"
            "<td><input type='Password' size=25 name='pwd'><br></td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
            "<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
        "</tr>"
    "</table>"
"</form>"
"<script>"
    "function check(form)"
    "{"
    "if(form.userid.value=='admin' && form.pwd.value=='admin')"
    "{"
    "window.open('/serverIndex')"
    "}"
    "else"
    "{"
    " alert('Error Password or Username')/*displays error message*/"
    "}"
    "}"
"</script>";
 
/*
 * Server Index Page
 */
 
const char* 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'>"
        "<input type='submit' value='Update'>"
    "</form>"
 "<div id='prg'>progress: 0%</div>"
 "<script>"
  "$('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) + '%');"
  "}"
  "}, false);"
  "return xhr;"
  "},"
  "success:function(d, s) {"
  "console.log('success!')" 
 "},"
 "error: function (a, b, c) {"
 "}"
 "});"
 "});"
 "</script>";
/*
 * setup function
 */
void setup(void) {
  M5.begin();
  M5.Lcd.fillScreen(RED);
  M5.Lcd.print("Hello World");
  // Connect to WiFi network
  WiFi.begin(ssid, password);
  Serial.println("");
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  /*use mdns for host name resolution*/
  if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
  /*return index page which is stored in serverIndex */
  server.on("/", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", loginIndex);
  });
  server.on("/serverIndex", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", serverIndex);
  });
  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.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);
      }
    }
  });
  server.begin();
}
void loop(void) {
  server.handleClient();
  delay(1);
}

スケッチ例のssidとpasswordだけ書き換えることで動かせます。 こちらもM5StickC用に画面初期化だけ追加してあります。

ESP32のIPアドレスにブラウザでアクセスすると、ログイン画面がでます。IDにadmin、パスワードにadminを指定してログインをすると、ファイルアップロード画面になりますので、BINファイルを指定して更新します。

アップロードが完了すると、再起動してプログラムが更新されます。画面の初期化色を変更するなどして、プログラムが更新されたかを確認してください。

Update.write()でファイルをOTA領域に書き込んでいますので、同じようにして、サーバーから最新プログラムを自動更新するなどの機能も作れそうです。

まとめ

急ぎ足になりましたが、Wi-Fi経由でのプログラム更新ができました。Wi-Fiが汚れた環境にいるので、実は苦労していますが普通の環境の場合にはシリアル経由よりも高速で転送することも可能かもしれません。

ただし、Serialクラスでのデバッグ出力ができなくなりますので、Arduino IDE以外のアプリでシリアル通信を行うか、それ以外のデバッグ方法を採用する必要があります。

また、フラッシュ暗号化したESP32の場合、Arduino IDEでもOTA経由であればプログラム更新ができそうな気がしますので、今後使えなくなってもいいESP32を準備して検証をしてみたいと思います。

コメント