M5StickCバッテリーライフ検証 その2

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

概要

その1 に引き続き、バッテリー駆動時間の計測をしてみました。一分に1度データ転送をした場合の駆動時間です。

充電時間

駆動時間とは違いますが、充電に約40分程度かかっています。バッテリーの状態や、本体の温度など状況によって変わると思いますが、数分間同じ電圧になり上昇が止まったところまで測定しています。

実験中に3ボルト以下になって、自動的に電源が落ちた状態から、USB給電をして測定をしています。USB接続した瞬間に3.4ボルト程度に上がりますが、外部電源を外した瞬間に電圧は低下します。

充電完了時には4.18ボルト程度ですが、接続を外すと4.10ボルトなどに低下していました。

また、充電は電源を入れておいた方は早いはずです。M5.begin()関数でAXP192の充電速度のパラメータをセットしているためです。ただしセットしている充電速度までは出ていないので、本当に影響しているかは未検証です。

CPU速度別

条件動作分数
240MHz52
10MHz95183%
#include <M5StickC.h>

void setup() {
  setCpuFrequencyMhz(10);
  M5.begin();
  M5.Lcd.fillScreen(WHITE);
  Serial2.begin(115200, SERIAL_8N1, 0, 26);
}

void loop() {
  Serial2.println(M5.Axp.GetBatVoltage());
  delay(60000);
}

動作周波数を10MHzにして計測した結果です。過去の消費電流調査でも画面より、CPU周波数を下げたほうが省電力になるのがわかっていましたがかなり違います。

もう少し条件を変えて実験したいのですが、この条件だけでも充電と計測で2時間半ぐらいかかっています。。。

BluetoothSerial

条件動作分数
Base(Serial)52
BluetoothSerial4383%
BluetoothSerial(画面7, CPU80)90173%
#include <M5StickC.h>
#include <BluetoothSerial.h>

BluetoothSerial SerialBT;

void setup() {
  setCpuFrequencyMhz(80);
  M5.begin();
  M5.Axp.ScreenBreath(7);
  M5.Lcd.fillScreen(WHITE);
  SerialBT.begin("ESP32");
}

void loop() {
  SerialBT.println(M5.Axp.GetBatVoltage());
  delay(60000);
}

実線でのシリアル接続ではなく、BluetoothSerialを使った無線接続の計測です。無線を使っても思ったより消費電力は増えないので、小さいM5StickCでは積極的に利用したいと思います。

また、CPU周波数は無線を利用する場合には、80MHz以上を指定する必要があります。10、20、40を指定するとウォッチドッグタイマでリセットがかかりますので注意してください。

画面暗くして、CPU周波数を下げれば、実運用でも1時間以上毎分の通信ができそうです。

まとめ

実は毎秒通信のデータも計測したのですが、結果を保存せずに閉じてしまったので、再度測定しなおしです、、、

スリープ系の実験をしたいと思っていましたが、もう少しリアルタイム系の通信を計測したいと思います。Wi-fiはもう少し時系列が細かいデータが取れる機材が届いてから実験したいと思います。

M5StickCバッテリーライフ検証 その1

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

概要

M5StickCのバッテリー駆動時間を、条件を変えながら計測してみました。

測定条件として2台のM5StickCをシリアル接続し、60秒間隔でバッテリー電圧を記録しました。

充電管理が難しいので、バッテリー電圧が4.0V以下になった計測から、電源が切れるまでの記録を比べています。

同一端末を利用して、計測を行っていますが誤差や、個体差などがあるので参考値として利用してください。

ベース条件

#include <M5StickC.h>

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(WHITE);
  Serial2.begin(115200, SERIAL_8N1, 0, 26);
}

void loop() {
  Serial2.println(M5.Axp.GetBatVoltage());
  delay(60000);
}

上記プログラムをベースに作っています。画面は起動がわかりやすい白に設定し、0と26ピンを利用してシリアルで現在のバッテリー電圧を60秒間隔で出力しています。

受信プログラム

#include <M5StickC.h>

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(WHITE);
  Serial2.begin(115200, SERIAL_8N1, 0, 26);
}

void loop() {
  if (Serial2.available()) {
    int inByte = Serial2.read();
    Serial.write(inByte);
  }
}

受信側のプログラムです。送信側とはクロスでTXとRXを接続して、外部シリアルの受信を、USB接続のシリアルに転送するプログラムです。

画面の明るさ別バッテリーライフ

#include <M5StickC.h>

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(7);
  M5.Lcd.fillScreen(WHITE);
  Serial2.begin(115200, SERIAL_8N1, 0, 26);
}

void loop() {
  Serial2.println(M5.Axp.GetBatVoltage());
  delay(60000);
}

上記のようにM5.Axp.ScreenBreath()関数で液晶画面の明るさを変更して、動作時間を比べてみました。数値が低いほど暗く、7がうっすら見える限界値ですが、9ぐらいの明るさからが実用範囲内と思います。

条件動作分数
Base(液晶明るさ12)52
液晶明るさ973140%
液晶明るさ882158%
液晶明るさ785163%

標準値の12だと、60分も動作することができませんでした。7だとさすがに液晶が読みにくいので、画面表示するのであれば、8か9ぐらいを使いたいですね。

まとめ

内部の消費電力も一緒に出力しておいたほうが、後の分析に便利そうですがこの条件で計測をはじめてしまったので、過去の計測値などから、参考値をどこかで計算したいと思います。

液晶の他にCPU周波数や、スリープ時などの計測も続ける予定です。しかしながら充電に30分以上かかるのと、測定時間だけでも5時間以上かかっているので、のんびり計測を続けていきたいと思います。

Arduino用BDFフォントデータefontをライブラリ登録しました

概要

/efont/さんのBDFフォントを利用させていただいた、上記のフォントライブラリをArduino IDEのライブラリに登録しました。

手順

上記サイトに詳細な手順がまとまっています。

他のライブラリを参考にしてものを作って、追加申請すれば結構かんたんに承認してくれました。ただし、スケッチ例がM5StickCだけという手抜き仕様。。。

まとめ

みなさん、かんたんにライブラリ登録できるので、どんどん登録してみてはいかがでしょうか?

M5StickC SDK0.1.1版 AXP192クラスの使い方

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

Doxygenでの自動作成のリファレンスはこちらです。

概要

SDKに大幅に変更があった0.1.1対応のAXP192クラスの使い方をまとめました。

内部ステップ数を返却する関数群が非推奨になり、計算後の数値がそのまま利用できるようになりました。

サンプルスケッチ

#include <M5StickC.h>

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);
}

void loop() {
  M5.Lcd.setCursor(0, 4, 1);

  M5.Lcd.printf("AXP192 Test\n");
  M5.Lcd.printf("\n");

  M5.Lcd.printf("Battery\n");
  M5.Lcd.printf(" State:%6d\n"  , M5.Axp.GetBatState());      // バッテリーが接続されているか(常に1のはず)
  M5.Lcd.printf(" Warn :%6d\n"  , M5.Axp.GetWarningLevel());  // バッテリー残量警告 0:残あり, 1:残なし
  M5.Lcd.printf(" Temp :%6.1f\n", M5.Axp.GetTempInAXP192());  // AXP192の内部温度
  M5.Lcd.printf(" V(V) :%6.3f\n", M5.Axp.GetBatVoltage());    // バッテリー電圧(3.0V-4.2V程度)
  M5.Lcd.printf(" I(mA):%6.1f\n", M5.Axp.GetBatCurrent());    // バッテリー電流(プラスが充電、マイナスが放電)
  M5.Lcd.printf(" W(mW):%6.1f\n", M5.Axp.GetBatPower());      // バッテリー電力(W=V*abs(I))

  M5.Lcd.printf("ASP\n");
  M5.Lcd.printf(" V(V) :%6.3f\n", M5.Axp.GetAPSVoltage());    // ESP32に供給されている電圧

  M5.Lcd.printf("VBus(USB)\n");
  M5.Lcd.printf(" V(V) :%6.3f\n", M5.Axp.GetVBusVoltage());   // USB電源からの電圧
  M5.Lcd.printf(" I(mA):%6.1f\n", M5.Axp.GetVBusCurrent());   // USB電源からの電流

  M5.Lcd.printf("VIN(5V-In)\n");
  M5.Lcd.printf(" V(V) :%6.3f\n", M5.Axp.GetVinVoltage());    // 5V IN端子からの電圧
  M5.Lcd.printf(" I(mA):%6.1f\n", M5.Axp.GetVinCurrent());    // 5V IN端子からの電流

  delay(1000);
}

SetLDO2について

明るさ消費電流(mA)増減
OFF63.1-31.3
063.2-31.2
163.2-31.2
263.2-31.2
363.2-31.2
463.2-31.2
563.2-31.2
663.2-31.2
763.4-31.0
865.9-28.5
969.3-25.1
1075.7-18.7
1183.6-10.8
1294.40.0

液晶をON、OFFできる関数が追加されましたが、実際の消費電力を比べたところ、それほど減りませんでした。現状OFFにするぐらいだったら、明るさ7とかで表示するのがおすすめです。

まとめ

全般的に素直に利用できるようになっています。ただ充電クーロン系関数は0.1.1では、正常に動かないので注意してください。上の図にある関数以外で使う関数は、ボタンの状態取得ぐらいだと思います。

細かい初期化時のパラメータも変更されており、MICに給電される電圧が2.8Vから3.3Vになったりと、普通に使う分には影響がないと思います。

あとは、クーロンカウンターが初期化時に有効化されなくなり、EnableCoulombcounter()を呼び出す必要がありますが、クーロン系関数が正常に動いていないのと、普段使うことはないと思います。

M5StickC SDK 0.1.1 AXP192クラス変更点解析

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

Doxygenでの自動作成のリファレンスはこちらです。

概要

AXP192クラスは大幅に変更が入っています。I2Cのアクセス関数がきれいなラッパー関数経由に変更されているのと、内部ADCのステップ数を返却していた関数が非推奨になり、計算後の数値を返す関数群が追加されています。

追加関数群

バッテリー存在チェック GetBatState()

バッテリーの存在チェックをする関数。基本的にはバッテリーが存在しない場合は無いはずだが、M5StickC以外でAXP192を使った場合に接続されているか確認するような用途だと思われます。

内部動作電圧取得 GetAPSVoltage()

GetVapsData()の値を返す版。バッテリー動作の場合には5Vから4V程度に下がる電圧です。

バッテリー充電電流取得 GetBatChargeCurrent()

GetIchargeData()の値を返す版。充電している電流値が取得できます。

バッテリー充電クーロン取得 GetBatCoulombInput()

GetCoulombchargeData()の値を返す版。普通は利用しないと思います。

バッテリー放電クーロン取得 GetBatCoulombOut()

GetCoulombdischargeData()の値を返す版。普通は利用しないと思います。

バッテリー電流取得 GetBatCurrent()

充電電流と放電電流の差から計算。プラスだと充電中でマイナスだと放電中。

バッテリー電圧取得 GetBatVoltage()

GetVbatData()の値を返す版。3.0Vから4.2Vぐらいの範囲になるはずです。

バッテリー瞬間電力取得 GetBatPower()

GetPowerbatData()の値を返す版。普通は利用しないと思います。

バッテリークーロン初期化 SetCoulombClear()

名前はクリアですが、AXP192のデータシートでは一時停止な気がします。普通は利用しないと思います。

AXP192温度取得 GetTempInAXP192()

GetTempData()の値を返す版。AXP192の温度なので、気温とはかなり異なります。ESP32の内部温度とそれほど変わらないはずなので、普通は利用しないと思います。

USB電源電流取得 GetVBusCurrent()

GetIusbinData()の値を返す版。USBからAXP192の内部名のVBusに名称が変更されています。

USB電源電圧取得 GetVBusVoltage()

GetVusbinData()の値を返す版。USBからAXP192の内部名のVBusに名称が変更されています。

外部電源電流取得 GetVinCurrent()

GetIinData()の値を返す版。

外部電源電圧取得 GetVinVoltage()

GetVinData()の値を返す版。

低電圧状態チェック GetWarningLevel()

GetWarningLeve(void)が既存ですが、Typoだったみたいで最後にlが追加されている関数が追加されています。そして既存関数は非推奨になっていません。。。そして既存関数だけ内部処理がラッパー関数を使っていない古い処理です。。。

LDO2(液晶)利用設定 SetLDO2(bool State)

明るさ制御ではなく、液晶自体へ電圧出力しない設定が追加されました。

まとめ

AXP192は0.1.1のリリース直前にがっつり更新されたので、予想外の更新でした。他のプロダクトやUIFlowとかの兼ね合いで統一したのかな?

全体的には使いやすくなったと思いますが、資料を全面的に書き換えを実施したいと思います。

液晶OFFが選択できるようになったので、後ほど最低の明るさとOFFでの電流差も測定したいと思います。

M5StickC SDK 0.1.1リリース

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

Doxygenでの自動作成のリファレンスはこちらです。

概要

新バージョンのSDKがリリースされたので、内容を確認しました。

M5StickCは加速度センサーがSH200QとMPU6886の二種類ありますが、今回のバージョンからラッパークラスを使うことで、どちらのIMUでも同じようにプログラムすることができるようになりました。

結構大きな変更点なのですが、PRを出したら取り込んでくれたのですが、8月20日にマージされて、リリースがやっと本日でした!

主な変更点

  • IMUクラスがラッパークラスとなり内部でSH200QとMPU6886クラスを振り分け
  • AXPがいろいろ変更
  • コンパイル時の警告が表示されないように

IMUクラス

いままでSH200QはIMUクラスで、MPU6886がMPU6886クラスという非対称のクラス名でしたが、IMUクラスがラッパー関数となり、搭載しているIMUに応じて、SH200QクラスとMPU6886クラスの関数を呼び分ける仕様に変わりました。

この変更によって、従来のIMUクラスを利用しているコードのままで、搭載しているIMUがSH200QとMPU6886を意識せずに、なんとなく動くようになっています。

なぜなんとなくと書いたかというと、このプルリクエストはわかりやすいベタ書きコードで、細かい数値補正などはせず、まずは取り込んでもらって、その後で微調整をすればいいかなと思ったからです。

本来的にIMUのハードが違うのと、設定値が微妙に違うので同じような数値がでているようで、ちょっと違う数値になっています。

AXP192クラス

いろいろ変わっています。I2Cへのアクセスがベタ書きだったのですが、ラッパー関数を作って大幅に書き直しています。そのため行ベースでのDIFFだと差分がわかりにくいので、じっくりと後日解析したいと思います。

今まで数値が内部のステップ数を返却していて、データシート見ながら謎の係数をかけていましたが、そのまま使える数値を返却する関数群が増えています!

コンパイル時の警告抑制

コンパイル時の警告が結構でていたのを、何個か潰しました。ESP32のライブラリも警告が何個かでるので、あまり中国の人は警告見てないのかな?

その他

AXP192のスケッチ例が、関数群入れ替えのため更新されていますが、スクリプトを使って描画されていました。スクリプト関数を使った例は初めてだったと思います。

HATやUNITも新販売したものは追加されていますが、Beetlecとかの動くもののコードは何故か別の場所なんですよね。

まとめ

HATは新製品が毎週でているのに、ライブラリが更新されていなかったM5StickC。待ちに待ったアップロードですが、次回はもう少し短いスパンで更新して欲しいです。

ESP32のOTA方式

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

概要

OTAに関するスケッチ例がいろんな場所にあったので、すべて確認してみました。大きく分けてespota.pyを利用するArduinoOTA方式と、ファイルをフラッシュに書き込むUpdate方式がありました。

方式解説

ArduinoOTA

ESP32側は3232ポートで待ち受けており、espota.pyなどを利用してファイルを転送します。転送時にはパスワードか、パスワードハッシュで認証を行うことが可能です。

一般的にインターネット経由での更新はできませんが、Arduino IDEからはシリアルポートの変わりにESP32のIPアドレスを選択することで、プログラム更新ができます。

デメリットとしてはArduino IDE側の制約により、シリアル接続ができなくなるので、シリアルモニタが利用できません。Arduino IDE以外のシリアルエディタなどで、シリアル出力を表示することは可能です。

Update

フラッシュにプログラムを書き込むことで更新します。プログラムファイルはブラウザからアップロードしたり、SDからロードしたり、Webサーバーからダウンロードすることが可能です。

インターネット上のサーバーに最新バージョンを置いておき、更新するような用途で利用しやすい方式です。

Arduino IDEからはプログラムを更新することができませんので、シリアル経由で更新するか、別途更新ツールを利用して更新する必要があります。

スケッチ例一覧

カテゴリスケッチ名方式概要
ArduinoOTABasicOTAArduinoOTA.h一般的なOTA(espota.py利用)
WebServerHttpBasicAuthArduinoOTA.hOTA+標準的なBASIC認証
WebServerHttpAdvancedAuthArduinoOTA.hOTA+ダイアログ文字列指定BASIC認証
ArduinoOTAOTAWebUpdaterUpdate.hブラウザからBINファイルアップロード更新
WebServerWebUpdateUpdate.hログイン画面がない単純なOTAWebUpdater
UpdateAWS_S3_OTA_UpdateUpdate.hHTTPからのBINファイルダウンロード更新
UpdateSD_UpdateUpdate.hSDカードからの更新

BasicOTA

一番標準的なOTAの例です。ArduinoOTAに対応する場合には、このスケッチをベースに拡張する方法がよいです。

HttpBasicAuth、HttpAdvancedAuth

BASIC認証のサンプルです。OTAも可能ですが、OTAのサンプルとしては考慮しないほうがよいです。

OTAWebUpdater

Update系の標準的なサンプルですが、認証まわりが入っているので少しわかりにくいコードです。ブラウザからファイルアップロードにてプログラム更新をします。認証もJavaScriptで行っているので、ブラウザのソースを見るとパスワードがわかってしまう実装です。

WebUpdate

一番シンプルなUpdateのサンプルです。基本処理はOTAWebUpdaterと変わらないですが、認証がない分スッキリしたコードになっています。

AWS_S3_OTA_Update

AWS S3と名前がついていますがS3に特化していることはなく、単にHTTPからプログラムファイルをダウンロードして、更新します。

HTTPSでのダウンロードを行う場合には、証明書のチェックを行わないか、WiFiClientSecureなどを参考にして、証明書を内蔵する必要があります。

サンプルの名前としては不適切で、単にHTTP_Updateなどの方が良かったと思います。

SD_Update

SDカードからプログラムファイルを読み出し、フラッシュに更新するサンプルです。インターネットに接続していない端末で、プログラムを更新するには便利そうですね。

まとめ

なんとなく名前で避けていたAWS_S3_OTA_Updateが、単なるダウンロードだったので、HTTPダウンロードの検証ができてよかったです。

暗号化した端末でもちょっと試しましたが、OTAで送信するファイルは暗号化前で、転送後に自動暗号化してくれている気がしますが、ブートローダーのファイルチェックが暗号化に対応しておらず、不正イメージ扱いにされてOTA失敗になっているようでした。

ブートローダーに手を入れるのはちょっと面倒なので、暗号化とOTAを組み合わせる場合にはArduinoではなく、ESP-IDFで開発したほうが良さそうです。

Arduino core for the ESP32の暗号化検証

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

概要

ESP32の暗号化機能を検証しました。

ESP-IDFですと、正式サポートされていますが、Arduino coreでは暗号化処理や転送をコマンドラインで行う必要がありました。

暗号化とは

ESP32は内部に暗号化キーを保存することができ、その暗号化キーを利用してフラッシュメモリの暗号化が可能です。

暗号化キーは外部からアクセスできない領域に保存されており、一度だけしか書き込むことができません。

暗号化キー作成

C:\Temp>espsecure.py generate_flash_encryption_key ./key.bin
espsecure.py v2.7
Writing 256 random bits to key file ./key.bin

上記コマンドで暗号化キーを作成します。内容は32バイトのランダムな数値のバイナリファイルです。この暗号化キーは端末ごとに変更するのが推奨で、MACアドレスなどでファイル名をつけて保存しておいたほうが安全です。

同じキーを使い回すことも可能ですが、そのキーですべて暗号化できてしまうので、キーの管理には気をつけてください。

プログラム作成と転送

普通にプログラムを作成します。環境設定の「より詳細な情報を表示する」の「書き込み」をチェックしてファイル転送をしてください。

最大1310720バイトのフラッシュメモリのうち、スケッチが734366バイト(56%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が43216バイト(13%)を使っていて、ローカル変数で284464バイト使うことができます。
C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\esp32\tools\esptool_py\2.6.1/esptool.exe --chip esp32 --port COM4 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0xe000 C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4/tools/partitions/boot_app0.bin 0x1000 C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4/tools/sdk/bin/bootloader_dio_80m.bin 0x10000 C:\Users\%USERNAME%\AppData\Local\Temp\arduino_build_282952/BasicOTA_WL.ino.bin 0x8000 C:\Users\%USERNAME%\AppData\Local\Temp\arduino_build_282952/BasicOTA_WL.ino.partitions.bin 
esptool.py v2.6
Serial port COM4
Connecting........__
Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
MAC: ??:??:??:??:??:??
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 8192 bytes to 47...

Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.0 seconds (effective 5957.7 kbit/s)...
Hash of data verified.
Compressed 15856 bytes to 10276...

Writing at 0x00001000... (100 %)
Wrote 15856 bytes (10276 compressed) at 0x00001000 in 0.1 seconds (effective 961.0 kbit/s)...
Hash of data verified.
Compressed 734480 bytes to 412789...

Writing at 0x00010000... (3 %)
Writing at 0x00014000... (7 %)
Writing at 0x00018000... (11 %)
Writing at 0x0001c000... (15 %)
Writing at 0x00020000... (19 %)
Writing at 0x00024000... (23 %)
Writing at 0x00028000... (26 %)
Writing at 0x0002c000... (30 %)
Writing at 0x00030000... (34 %)
Writing at 0x00034000... (38 %)
Writing at 0x00038000... (42 %)
Writing at 0x0003c000... (46 %)
Writing at 0x00040000... (50 %)
Writing at 0x00044000... (53 %)
Writing at 0x00048000... (57 %)
Writing at 0x0004c000... (61 %)
Writing at 0x00050000... (65 %)
Writing at 0x00054000... (69 %)
Writing at 0x00058000... (73 %)
Writing at 0x0005c000... (76 %)
Writing at 0x00060000... (80 %)
Writing at 0x00064000... (84 %)
Writing at 0x00068000... (88 %)
Writing at 0x0006c000... (92 %)
Writing at 0x00070000... (96 %)
Writing at 0x00074000... (100 %)
Wrote 734480 bytes (412789 compressed) at 0x00010000 in 7.0 seconds (effective 838.9 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 128...

Writing at 0x00008000... (100 %)
Wrote 3072 bytes (128 compressed) at 0x00008000 in 0.0 seconds (effective 2457.6 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

上記が転送ログです、上の方にある転送している行だけ抜き出します。

C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\esp32\tools\esptool_py\2.6.1/esptool.exe
--chip
esp32
--port
COM4
--baud
921600
--before
default_reset
--after
hard_reset
write_flash
-z
--flash_mode
dio
--flash_freq
80m
--flash_size
detect
0xe000
C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4/tools/partitions/boot_app0.bin
0x1000
C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4/tools/sdk/bin/bootloader_dio_80m.bin
0x10000
C:\Users\%USERNAME%\AppData\Local\Temp\arduino_build_282952/BasicOTA_WL.ino.bin
0x8000
C:\Users\%USERNAME%\AppData\Local\Temp\arduino_build_282952/BasicOTA_WL.ino.partitions.bin

上記がスペースを改行に置き換えたものです。4つのファイルを転送しているのがわかります。

Arduinoはビルドした際にディレクトリが毎回変わりますので、ビルドログや転送ログを確認してどのディレクトリにファイルが作成されているかを確認します。

  • (0xe000)boot_app0.bin
  • (0x1000)bootloader_dio_80m.bin
  • (0x10000)BasicOTA_WL.ino.bin
  • (0x8000)BasicOTA_WL.ino.partitions.bin

上記の4つで、パーティション情報はデフォルトですので下の情報になります。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x140000,
app1,     app,  ota_1,   0x150000,0x140000,
spiffs,   data, spiffs,  0x290000,0x170000,

nvs(0x9000)

nvsは内部データ保存領域ですが、暗号化対象外です。ファイル転送をせずにプログラムから初期化して利用します。

otadata(0xe000)

OTAで起動するのがapp0かapp1かを保存する領域です。暗号化対象外です。

app0(0x10000)

実際のプログラム領域です。暗号化してから転送する必要があります。

app1(0x150000)

OTAで保存するプログラム領域です。最初はapp0を利用するので、app1は利用しません。

spiffs(0x290000)

SPIフラッシュを利用したファイル領域です。暗号化対象外です。利用する場合には通常と同じようにファイルを転送してください。

bootloader(0x1000)

標準領域なのでパーティションには情報がありませんが、2ndブートローダーの領域です。こちらも暗号化して転送する必要があります。

ESP32の状態取得

C:\Temp>espefuse.py -p COM4 summary
espefuse.py v2.7
Connecting....
EFUSE_NAME             Description = [Meaningful Value] [Readable/Writeable] (Hex Value)
----------------------------------------------------------------------------------------
Security fuses:
FLASH_CRYPT_CNT        Flash encryption mode counter                     = 0 R/W (0x0)
FLASH_CRYPT_CONFIG     Flash encryption config (key tweak bits)          = 0 R/W (0x0)
CONSOLE_DEBUG_DISABLE  Disable ROM BASIC interpreter fallback            = 1 R/W (0x1)
ABS_DONE_0             secure boot enabled for bootloader                = 0 R/W (0x0)
ABS_DONE_1             secure boot abstract 1 locked                     = 0 R/W (0x0)
JTAG_DISABLE           Disable JTAG                                      = 0 R/W (0x0)
DISABLE_DL_ENCRYPT     Disable flash encryption in UART bootloader       = 0 R/W (0x0)
DISABLE_DL_DECRYPT     Disable flash decryption in UART bootloader       = 0 R/W (0x0)
DISABLE_DL_CACHE       Disable flash cache in UART bootloader            = 0 R/W (0x0)
BLK1                   Flash encryption key
  = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W
BLK2                   Secure boot key
  = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W
BLK3                   Variable Block 3
  = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W

Config fuses:
XPD_SDIO_FORCE         Ignore MTDI pin (GPIO12) for VDD_SDIO on reset    = 0 R/W (0x0)
XPD_SDIO_REG           If XPD_SDIO_FORCE, enable VDD_SDIO reg on reset   = 0 R/W (0x0)
XPD_SDIO_TIEH          If XPD_SDIO_FORCE &amp; XPD_SDIO_REG, 1=3.3V 0=1.8V   = 0 R/W (0x0)
CLK8M_FREQ             8MHz clock freq override                          = 54 R/W (0x36)
SPI_PAD_CONFIG_CLK     Override SD_CLK pad (GPIO6/SPICLK)                = 0 R/W (0x0)
SPI_PAD_CONFIG_Q       Override SD_DATA_0 pad (GPIO7/SPIQ)               = 0 R/W (0x0)
SPI_PAD_CONFIG_D       Override SD_DATA_1 pad (GPIO8/SPID)               = 0 R/W (0x0)
SPI_PAD_CONFIG_HD      Override SD_DATA_2 pad (GPIO9/SPIHD)              = 0 R/W (0x0)
SPI_PAD_CONFIG_CS0     Override SD_CMD pad (GPIO11/SPICS0)               = 0 R/W (0x0)
DISABLE_SDIO_HOST      Disable SDIO host                                 = 0 R/W (0x0)

Identity fuses:
MAC                    Factory MAC Address
  = ??:??:??:??:??:?? (CRC 66 OK) R/W
CHIP_VER_REV1          Silicon Revision 1                                = 1 R/W (0x1)
CHIP_VERSION           Reserved for future chip versions                 = 2 R/W (0x2)
CHIP_PACKAGE           Chip package identifier                           = 0 R/W (0x0)

Calibration fuses:
BLK3_PART_RESERVE      BLOCK3 partially served for ADC calibration data  = 0 R/W (0x0)
ADC_VREF               Voltage reference calibration                     = 1107 R/W (0x1)

Efuse fuses:
WR_DIS                 Efuse write disable mask                          = 0 R/W (0x0)
RD_DIS                 Efuse read disablemask                            = 0 R/W (0x0)
CODING_SCHEME          Efuse variable block length scheme                = 0 R/W (0x0)
KEY_STATUS             Usage of efuse block 3 (reserved)                 = 0 R/W (0x0)

Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V).

必須ではありませんが、作業前にボードの状態を表示して、どこが変わったのかを確認します。

暗号化キーの書き込み

C:\Temp>espefuse.py -p COM4 burn_key flash_encryption ./key.bin
espefuse.py v2.7
Connecting....
Write key in efuse block 1. The key block will be read and write protected (no further changes or readback). This is an irreversible operation.
Type 'BURN' (all capitals) to continue.
BURN
Burned key data. New value: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Disabling read/write to key efuse block...

暗号化キーを書き込みます。一度書き込んだら変更ができません。失敗すると使うことができなくなるので注意してください。

上記は検証用に全部00で埋めた暗号化キーを書き込んでいます。

暗号化を有効にする

C:\Temp>espefuse.py -p COM4 burn_efuse FLASH_CRYPT_CNT
espefuse.py v2.7
Connecting......
Burning efuse FLASH_CRYPT_CNT (Flash encryption mode counter) 0x0 -> 0x1. This is an irreversible operation.
Type 'BURN' (all capitals) to continue.
BURN

暗号化を有効にします。立っているビット数が奇数の場合には暗号化が有効になります。再度実行すると0x03になって無効になります。最高4回暗号化の有効化と無効化ができますが、4回目に無効にしたところで、それ以上変更できなくなります。

基本的には1度有効化したものは無効化しない運用がおすすめです。

暗号化モード設定

C:\Temp>espefuse.py -p COM4 burn_efuse FLASH_CRYPT_CONFIG 15
espefuse.py v2.7
Connecting.....
Burning efuse FLASH_CRYPT_CONFIG (Flash encryption config (key tweak bits)) 0x0 -> 0xf. This is an irreversible operation.
Type 'BURN' (all capitals) to continue.
BURN

暗号化の強度を設定します。基本的には15で利用してください。

状態確認

C:\Temp>espefuse.py -p COM4 summary
espefuse.py v2.7
Connecting......
EFUSE_NAME             Description = [Meaningful Value] [Readable/Writeable] (Hex Value)
----------------------------------------------------------------------------------------
Security fuses:
FLASH_CRYPT_CNT        Flash encryption mode counter                     = 1 R/W (0x1)
FLASH_CRYPT_CONFIG     Flash encryption config (key tweak bits)          = 15 R/W (0xf)
CONSOLE_DEBUG_DISABLE  Disable ROM BASIC interpreter fallback            = 1 R/W (0x1)
ABS_DONE_0             secure boot enabled for bootloader                = 0 R/W (0x0)
ABS_DONE_1             secure boot abstract 1 locked                     = 0 R/W (0x0)
JTAG_DISABLE           Disable JTAG                                      = 0 R/W (0x0)
DISABLE_DL_ENCRYPT     Disable flash encryption in UART bootloader       = 0 R/W (0x0)
DISABLE_DL_DECRYPT     Disable flash decryption in UART bootloader       = 0 R/W (0x0)
DISABLE_DL_CACHE       Disable flash cache in UART bootloader            = 0 R/W (0x0)
BLK1                   Flash encryption key
  = ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? -/-
BLK2                   Secure boot key
  = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W
BLK3                   Variable Block 3
  = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W

Efuse fuses:
WR_DIS                 Efuse write disable mask                          = 128 R/W (0x80)
RD_DIS                 Efuse read disablemask                            = 1 R/W (0x1)
CODING_SCHEME          Efuse variable block length scheme                = 0 R/W (0x0)
KEY_STATUS             Usage of efuse block 3 (reserved)                 = 0 R/W (0x0)

Calibration fuses:
BLK3_PART_RESERVE      BLOCK3 partially served for ADC calibration data  = 0 R/W (0x0)
ADC_VREF               Voltage reference calibration                     = 1107 R/W (0x1)

Config fuses:
XPD_SDIO_FORCE         Ignore MTDI pin (GPIO12) for VDD_SDIO on reset    = 0 R/W (0x0)
XPD_SDIO_REG           If XPD_SDIO_FORCE, enable VDD_SDIO reg on reset   = 0 R/W (0x0)
XPD_SDIO_TIEH          If XPD_SDIO_FORCE &amp; XPD_SDIO_REG, 1=3.3V 0=1.8V   = 0 R/W (0x0)
CLK8M_FREQ             8MHz clock freq override                          = 54 R/W (0x36)
SPI_PAD_CONFIG_CLK     Override SD_CLK pad (GPIO6/SPICLK)                = 0 R/W (0x0)
SPI_PAD_CONFIG_Q       Override SD_DATA_0 pad (GPIO7/SPIQ)               = 0 R/W (0x0)
SPI_PAD_CONFIG_D       Override SD_DATA_1 pad (GPIO8/SPID)               = 0 R/W (0x0)
SPI_PAD_CONFIG_HD      Override SD_DATA_2 pad (GPIO9/SPIHD)              = 0 R/W (0x0)
SPI_PAD_CONFIG_CS0     Override SD_CMD pad (GPIO11/SPICS0)               = 0 R/W (0x0)
DISABLE_SDIO_HOST      Disable SDIO host                                 = 0 R/W (0x0)

Identity fuses:
MAC                    Factory MAC Address
  = ??:??:??:??:??:?? (CRC 66 OK) R/W
CHIP_VER_REV1          Silicon Revision 1                                = 1 R/W (0x1)
CHIP_VERSION           Reserved for future chip versions                 = 2 R/W (0x2)
CHIP_PACKAGE           Chip package identifier                           = 0 R/W (0x0)

Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V).

暗号化キーが設定されており、読み出せないので??で表示されています。FLASH_CRYPT_CNTとFLASH_CRYPT_CONFIGも変更されていることを確認してください。

暗号化を有効化してあるので、この状態でESP32を起動しても、エラーがでて起動しません。

ファイル暗号化

C:\Temp>espsecure.py encrypt_flash_data --keyfile ./key.bin --address 0x1000 -o bootloader.bin bootloader_dio_80m.bin
espsecure.py v2.7
Using 256-bit key

C:\Temp>espsecure.py encrypt_flash_data --keyfile ./key.bin --address 0x10000 -o app.bin BasicOTA.ino.bin
espsecure.py v2.7
Using 256-bit key

C:\Temp>espsecure.py encrypt_flash_data --keyfile ./key.bin --address 0x8000 -o partitions.bin BasicOTA.ino.partitions.bin
espsecure.py v2.7
Using 256-bit key

各種必要なファイルをコピーしてきて、暗号化処理を行います。アドレスの引数はパーティションの開始アドレスに合わせる必要があります。

暗号化する必要があるのはotadataを除く3つのファイルです。

ファイル転送

C:\Temp>esptool.py --chip esp32 --port COM4 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0xe000 boot_app0.bin 0x1000 bootloader.bin 0x10000 app.bin 0x8000 partitions.bin
esptool.py v2.7
Serial port COM4
Connecting....
Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
WARNING: Detected crystal freq 41.01MHz is quite different to normalized freq 40MHz. Unsupported crystal in use?
Crystal is 40MHz
MAC: ??:??:??:??:??:??
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 8192 bytes to 47...
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.0 seconds (effective 6571.6 kbit/s)...
Hash of data verified.
Warning: Image file at 0x1000 doesn't look like an image file, so not changing any flash settings.
Compressed 15856 bytes to 15867...
Wrote 15856 bytes (15867 compressed) at 0x00001000 in 0.2 seconds (effective 631.5 kbit/s)...
Hash of data verified.
Compressed 734480 bytes to 732412...
Wrote 734480 bytes (732412 compressed) at 0x00010000 in 9.7 seconds (effective 604.7 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 1801...
Wrote 3072 bytes (1801 compressed) at 0x00008000 in 0.0 seconds (effective 849.7 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

ファイルを転送します。Arduinoと同じ転送オプションで4つのファイルを転送します。一度転送すればapp0以外は変更されませんので、個別に転送しても大丈夫です。

動作確認

この状態で再起動して、動いていれば成功です!

失敗した場合には再度やり直すか、暗号化を諦めてFLASH_CRYPT_CNTをBURNすれば普通の状態に戻ります。

OTA

OTAは試してみましたが、Arduino core for the ESP32ではサポートされていない気がします。転送後のファイル内容確認の際に、暗号化されていない前提でチェックをしており転送エラーとなります。

暗号化したファイルをOTAで転送すると、転送中の序盤に転送エラーになりました。細かい動きを追っていませんが、ソース非公開の場所でのエラーですのでちょっと対応が難しい気がします。

少し検証したところ、ESP32のアプリは先頭が0xE9で始まっている必要があり、OTAの際にチェックしています。転送に成功しても暗号化されている状態なので、OTA初回起動時にチェックが失敗してOTAロールバックが実行されていました。

Arduinoのブートローダーは暗号化後のOTAには対応していないようでした。

参考サイト

この記事は上記サイトに書かれている内容を、Arduino core for the ESP32に特化して整理しなおしています。

まとめ

Arduino coreでのフラッシュ暗号化はできました。しかしながらArduino IDEからかんたんに転送できないので、非常に開発が面倒です。

せめてOTAが使えたら便利なのですが、普通に転送したところNGでした。ESP-IDFだとOTAが使えるので、なにか方法があるとは思いますが、現状は使えませんでした。

現状は開発は普通に行って、製品化などで暗号化したい場合の出荷時に暗号化を行うのが好ましいと思います。ただNVS領域の暗号化は、暗号化対応のNVS関連関数を呼び出す必要がありますので、そこはコマンドラインで転送しながら検証する必要があると思います。

これ以外にセキュアブートという機能もあるので、もう少し検証を続けたいと思います。

ESP32の棚卸し

概要

手元にあるESP32を整理しました。

一覧

obnizボード(上段一番左)

初期のobnizなので、BASICライセンスが入っているやつです。2018年6月6日にAmazonで4,980円で購入していますね。

今はAmazonでの直販はやっていないみたいなので、公式ストアから購入したほうがいいと思います。

JavaScriptを使って開発したいのであればobnizはおすすめです。個人的にはM5StickCの方がボードしては好きですが、まだobnizのサポートが弱い気がしますので、obnizボードを使ったほうが楽だと思います。

TTGO T-CAMERA(上段左から2個目)

カメラ付きで、技適を取っているものってなかなかないんですよね。これはESP32-WROVER-Bで、PSRAMを積んでいるのですが、圧倒的にI2Cのみと外部IOが少なすぎるので、使い勝手は悪いです。

DOIT型開発ボード(上段左から3個目)

一番ピン数が少ない開発ボードで、ミニブレッドボードに刺さるので、メインでつかっていたものです。古いものはWindowsで自動書き込みできなかったので、ENとGNDにコンデンサを入れていたのですが、新しいやつを試したら普通に自動書き込みできました!

ピン数が少ないので、JTAGとか特殊な用途には使えませんが、普段遣いには一番安くていいボードですね。

DevKitクローン?(上段左から4個目と5個目)

こちらもよく見る、ピン数が少し多いタイプの開発ボードです。細かいレイアウト違いがたくさんありますが、一般的な用途で利用するピンはすべて使えるはずです。

M5StickC(下段一番左)

現在のメインボード。数えたら4つ持っていました。現在Amazonでは正規流通版がないので、スイッチサイエンスさんとかで購入したほうがいいと思います。

WeMos Grove拡張(下段左から2個目と3個目)

これは日本で売っているのみたいことないです。WeMosと書いてありますが、おそらくTTGOが製造しているボードです。

Grove端子と、液晶がついているので使い勝手は非常にいいのですが、入手性に難があるのでおすすめできません。

D1 R32(下段左から4個目)

Arduino UNOと同じサイズのボードで、ジャンパー線を直接接続できるので、使い勝手がいいです。

まとめ

DOIT型が2つあったので、1つを暗号化検証用の生贄にしたいと思います。

沼が怖いので、M5Stackは持っていません!

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' &amp;&amp; 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&amp; 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を準備して検証をしてみたいと思います。