目次
背景
組み込みシステムとしてESP32を用いることが多いこの頃ですが、開発側としての要望として多いのが「無線使ってソフトウェアアップデートできるようにして欲しい」というものです。最初のうちはUART-USB経由でプログラムを書き込んでいても間に合う(試作品は多少スペースが空いていても良いし)のですが、周辺のメカニカルな部分ができあがってくると取り外すことが難しいところにESP32のボード一式が配置されてしまうことがあって。するとソフトウェアのバグによって、ごめんまたアップデートしたいからバラしてくれませんか、とは言いづらくなります。メカニカルな部分を自分で組み立てている場合でも、先の読めない自分を呪ってやりたい気分でバラして再度…となって精神衛生上よろしくないでしょう。ということで、OTA(Over The Air)アップデートを実装してソフトウェアアップデートは無線LANを経由してできるようにしたい、という欲望が生まれます。

できることなら避けて通りたかったけど…客先に出してしまうとアップデートがより困難になるので…仕方がないか。
OTAアップデートのサンプル
OTAアップデートのサンプル自体はespressif社からgithub公開はされています。HTTPS通信で最新バージョンを取得して、最新バージョンにて起動、初期化処理で失敗してしまったら正しくないアップデートだとして前のバージョンに戻して再起動、成功したらそのまま最新バージョンで動作し続ける、というものです。基本的にはこのサンプルの使い回して事足りるかなという所ではあります。

接続をしてPCとESP32を同一ネットワークにする…というのが典型例。
opensslを使ったWebサーバの用意
上記の仕組みを実現するためには、PC側がHTTPSサーバになってあげて、ESP32搭載基板からこのサーバにアクセス&ダウンロードさせる、というのが基本線になるだろうと思います。HTTPSサーバとしてはopenssl同梱のサーバを使うのが手が掛からないので良いのですが、諸々面倒を乗り越えなくてはならず…
- ESP32の開発環境ESP-IDFはMSYS2ベースなので、openssl起動させたとき標準入力・出力をWindows側のコマンドプロンプト等からもらってくる仕掛けを入れる必要がある。
- winptyがその仕掛けを持ったソフトウェアだが、git for windowsのインストールされた環境下だと(こちらもMSYS2ベースではあるけれど、ESP-IDFとはライブラリ等の互換性は考慮されていないので)混合させると管理が大変である。
- Windows 10 version 2004だとWSL2が非常に不安定…(涙)
という理由により、仕方がないのでHTTPSサーバは私の場合はgit for windowsの中で立てるか、docker for Windowsを持ち出すかの2択になってしまいました。この辺は別記事でも書きました。

いろんな開発環境を並列して持っているので、ライブラリの互換性等でハマりやすい状況になってるので…。いろんなものに手を出す八方美人っぷりが仇になってる…よね?
どのみち秘密鍵・公開鍵のペアをopensslを使って作ることになります。MSYS2で頑張る場合でもopenssl使えるので同じですけど、上記のwinptyを起動させて実行する必要があります。私はgit for Windowsを入れているので、git bashから実行しました。
まずはサーバ証明書(オレオレ証明書)を作ります。
openssl req -x509 -newkey rsa:2048 -keyout ca_key.pem -out ca_cert.pem -days 365 -nodes
組織の場所とかメールアドレスとか訊かれますが、まぁ適当な粒度で大丈夫ですね。ただ、ESP32側のプログラムによってはCNだけ気をつける必要があります(後述)。
その後で、HTTPSサーバを起動します。git for windowsのbashから実行するのであれば以下のような感じですかね。単純にファイル取得するだけならこんなのでも良いでしょう。
openssl s_server -WWW -key ca_key.pem -cert ca_cert.pem -port 8070
Windows 10 ProであればHyper-V使えるのでdockerでnginxコンテナ立ち上げてもできます(実はこれも試しました)。
ESP32側のプログラム用意
以下の項目を満足していくことが必要です。
パーティション
ESP-IDF開発環境では、プロジェクトフォルダの最上位にpartitions.csvファイルを置いておくとESP32内のフラッシュメモリ内にパーティションを切ってくれます。というとちょっと嘘が含まれていて、実際はidf.py menuconfig
を実行したときに現れるメニューから”Partition Table” -> “Partition Table(Custom Partition Table CSV)”を選択して、Custom partition table CSVを選んでおきます(典型的なOTAアップデートだけやりたい場合はFactory app, two OTA definitionsでも良いですけどね)。


で、このパーティションには少なくとも以下のパーティションが必要です。OTAアップデートをちゃんと実装するためには、2つパーティションが必要で、1個は今動いているプログラムの入った領域で、もう1個はダウンロードしてきた新しいプログラムを格納しておく領域になります。
- Type data, Subtype otaを指定したOTA用データ領域
ここにOTA用のパーティションのどちらが次に起動に使われるかの情報が入る。 - Subtype: ota_1, ota_2を指定したOTA用プログラム領域
単純ですが、こんな感じに分けて使っています。nvsは不揮発ストレージ領域(BluetoothやWifiの設定やADCのキャリブレーション値が入っています)、otadataがOTA用データ領域、factoryが一番最初に書き込んでおいたプログラムを格納する領域、ota_0, ota_1がOTAアップデートでダウンロードしてきたプログラムを格納する領域、です。プログラム容量が1MBでは厳しかったら、factoryは抜いても大丈夫で、その分増やすことはできます。storageはSPIFFS(フラッシュメモリ中に作ることができるファイルシステム)の領域です。なにもファイルベースで保存する気がないなら要らないですが、普通は何らか置いときたくなるものなので、一応領域を取っています。
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
ota_0, 0, ota_0, 0x110000, 1M,
ota_1, 0, ota_1, 0x210000, 1M,
storage, data, spiffs, 0x310000, 0x80000,
HTTPサーバの公開鍵書き込み
OTAアップデートはデフォルトではHTTPSで通信しますので、先ほど作ったHTTPサーバの公開鍵をESP32側でも持っている必要があります。
書き込み方はESP-IDFのidf.pyを使っていればCMakeが内部的には使われていますので、以下の記述をCMakefile.txtに対して行って公開鍵のpemファイルをイメージの末尾にくっつけます。${srcs}はコンパイル予定のソースファイルのリストですね…。
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "."
EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem
OTAを行う側のCプログラム側からは以下の記述をすることでpemファイルを読み込むことができます。
extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start")
これをHTTPSクライアントプログラムの中で使えば良いのですが、pemファイルを作成したとき入力したCN値とHTTPSクライアントプログラムでの接続先のサーバ名(IPアドレスでも可)とは同じにしておく必要があります。そうしないとOTA開始時にコケます。
esp_http_client_config_t config = {
.url = updateURL,
// URL文字列にてサーバ名を入れるが、server_cert_pem_startのCN値と同じである必要あり。http://192.168.0.1/ に接続するならCN値も192.168.0.1とする。
.cert_pem = (char *)server_cert_pem_start,
.use_global_ca_store = true,
.timeout_ms = 5000, .// 5秒くらい待ちましょうか
};

何というか、前準備が多いな…少し前から知ってはいたけど。
ESP32ソフトウェア側の処理
ここまでは設定の話で。残るのはESP32に書き込むソフトウェアもOTAアップデートに対応する必要があります。
OTAアップデートの結果不具合のあるコードがダウンロードされた場合ロールバックできるようにする
OTAアップデートを許すんだったら、不具合発生時のroll backは入れたくなるもの、かと思います。 プロジェクトフォルダの最上位にてidf.py menuconfig
を実行してCONFIG_BOOTLOADER_APP_ROLLBACK_ENABLEオプションをOnにするとロールバックできるようになります。その際、自作のESP32ソフトウェアにおいて以下のAPIを呼んであげる必要があります。
esp_ota_set_boot_partition()
:OTAにて自作ソフトウェアをダウンロード完了した際このAPIを呼んで、次回ブート時ダウンロードしたソフトウェアを格納したパーティションをbootloaderに読ませる。- 自作ソフトウェアのapp_main()関数内でセルフチェックを行い、無事に合格したら
esp_ota_mark_app_valid_cancel_rollback()
を呼んでリブートが発生しても現ソフトウェアをそのまま使い続けるモード(ESP_OTA_IMG_VALID)にする。 - 自作ソフトウェアのapp_main()関数内でのセルフチェックで不合格になった場合、
esp_ota_mark_app_invalid_rollback_and_reboot()
を呼んで、次回ブート時には以前動作していたソフトウェアを使うモード(ESP_OTA_IMG_INVALID)にする。
という辺りが本家のドキュメントでも箇条書きで表現されており読み取るのが面倒なので、ざっくり状態遷移図にしてみました。

OTAアップデート用のHTTPSクライアントを作る
ダウンロードしようにも、受け手がちゃんといないと成り立たない話なので、簡単なHTTPSクライアントを作る必要があります。ESP32の場合は、OTAのサンプル結構ありますので、そんな手間なく実装に盛り込むことはできます。
ただ、espressif社のサンプルコードを見ると、HTTPSクライアント部分のコードが外出しにしてあって少し隠蔽されているので(esp_err_t example_connect(void)
という名称で外出しされている)、最小限で起動可能なコードを一覧するのには結構神経が削れる気がします。私は製品候補のソフトウェアとしてOTAアップデートを作り込みたい、という立場なので、example_connect()
関数で行われていることも含めて取り込んであげることにしました。
イメージの読み込みとアップデート
最初は失敗…
私の書いていたプログラムの場合、BLE(Bluetooth Low Energy)を有効にしているため、そのままOTAアップデート用のWiFiを立ち上げると通信できなくなります(同一コアでソフトウェア実装されていて、かつ同一の周波数帯を使ってるからバッティングする、というのは分かりますがね…)。
何も考えずにWiFiを有効にすると、mTLS関連で通信開始処理ができなくなります。ESP32のメモリ不足と、ESP-IDFの制限?でWiFi側のイベントハンドラとBLE側のイベントハンドラが共存できないみたい。
I (29859) wifi:new:<6,0>, old:<1,0>, ap:<255,255>, sta:<6,0>, prof:1
I (30849) wifi:state: init -> auth (b0)
I (30869) wifi:state: auth -> assoc (0)
I (30879) wifi:state: assoc -> run (10)
I (30899) wifi:connected with kage_Wifi, aid = 11, channel 6, BW20, bssid = 9a:2c:bc:f1:40:91
I (30899) wifi:security: WPA2-PSK, phy: bgn, rssi: -26
I (30899) wifi:pm start, type: 1
I (30959) wifi:AP's beacon interval = 102400 us, DTIM period = 3
I (39019) esp_netif_handlers: OTA: sta ip: 192.168.137.112, mask: 255.255.255.0, gw: 192.168.137.1
I (39019) OTA: Got IP Addr:192.168.137.112
I (39019) OTA: Default Gateway:192.168.137.1
I (39029) OTA: Connecting https://192.168.137.1/update.bin
E (39579) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x4290
I (39579) esp-tls-mbedtls: Certificate verified.
E (39579) esp-tls: Failed to open new connection
E (39579) TRANS_SSL: Failed to open a new connection
E (39589) HTTP_CLIENT: Connection failed, sock < 0
E (39599) esp_https_ota: Failed to open HTTP connection: ESP_ERR_HTTP_CONNECT
E (39599) esp_https_ota: Failed to establish HTTP connection
E (39609) OTA: HTTPS OTA Begin failed!
BLEの無効化
そんなわけでOTAアップデートをするときにはBLEを落として(disableしてからdeinitする)WiFiを有効化する→WiFiで接続してOTAアップデート敢行→リブート後BLEを有効にする、というシーケンスが必要です。
ただ、BLEスタックをどこで無効化してスタックを消去するか、タイミング次第でOTAアップデートができないようです。私は以下のタイミングでしたら正しく外部HTTPサーバからファイルをダウンロードしてくることに成功しています(他の手順だとなぜかハングアップする)。
- WiFi関連のプロトコルスタック初期化等は最初にやってしまって良い。このとき、WiFi接続成功時のイベントハンドラ(on_wifi_connectコールバックとでもしておきます)を設定しておきます。
- OTAアップデートを行おうとするタイミングでesp_wifi_start()を呼び出してWiFi接続をスタートさせます。
- アクセスポイントに接続ができたタイミングで先に設定したイベントハンドラon_wifi_connectが呼び出されますが、この中でBLEの無効化を行います。
static void on_wifi_connect(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
esp_bluedroid_disable();
esp_bluedroid_deinit();
esp_bt_controller_disable();
esp_bt_controller_deinit();
}
static void wifi_init()
{
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ret = esp_wifi_init(&cfg)
esp_wifi_set_default_wifi_sta_handlers();
ret = esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &on_wifi_connect, NULL)
}
void ota_start()
{
...
esp_https_ota_config_t ota_config = {
.http_config = &config,
.http_client_init_cb = _http_client_init_cb,
};
esp_https_ota_handle_t https_ota_handle = NULL;
err = esp_https_ota_begin(&ota_config, &https_ota_handle);
...
}
イメージとしてはこんな感じでしょうか。