[ESP32]遠隔アップデートで異常発生時にロールバックさせる方法(OTA_ROLLBACK)

ESP32

遠隔アップデートで異常発生時にロールバックさせる方法

はじめに

ESP32のシステムを更新する場合、PCとESP32をmicroUSBで接続してシリアル接続を使用するのが一般的ですが、遠隔でシステム更新するための機能も備わっています。

1.Arduino IDE等の開発ツールからWifi経由で更新する方法
  → [ESP32]遠隔でシステム更新する方法(BasicOTA)
2.ESP32上でHTTPサーバを動作させておき、アプリをアップロードして更新する方法
  → [ESP32]遠隔でシステム更新する方法(OTAWebUpdater)
3.外部サーバにアプリをアップロードしておき、ESP32がダウンロードして更新する方法
  → 執筆中

今回は、上記のような遠隔でシステム更新した時に、アプリ不具合により再起動を繰り返すといったような場合に、前回のアプリへロールバックするようにします。

しっかりと確認していたつもりでも、設置済みのシステムへの遠隔更新は何があるかわかりません。もし、OTAが動作する前に再起動するような場合は、設置場所へ行く必要が出てくるので大変です。

開発環境

OS : Windows 11 Pro
ESP32:ESP-WROOM-32
統合開発環境 : Arduino IDE 2.1.0
Arduino core for the ESP32:1.0.6/2.0.17
使用ライブラリ:なし

[Arduino core for the ESP32:1.0.6]では、[CONFIG_APP_ROLLBACK_ENABLE]が無効になっています。ESP-IDF sdkconfig を変更する必要があります。
[ESP32]Arduino IDE で ESP-IDF の sdkconfig を変更する方法

[Arduino core for the ESP32:2.0.17]では、初期状態で有効になっています。

OTA_ROLLBACK について

ESP-IDF sdkconfig の [CONFIG_APP_ROLLBACK_ENABLE] を有効にすることで、動作します。
アプリに状態が付与され、その状態によりロールバック可否が決定します。状態遷移がいまいちよくわからなかったので、遷移図を作ってみました。6つの状態が存在するようです。

※Arduino IDE からアプリ更新した場合は、ロールバックされません。

使用関数

esp_ota_get_running_partition()
現在実行中のアプリのパーティション情報を取得します。

引数説明
なし
戻り値パーティション構造の情報へのポインタ。
パーティションが見つからないかフラッシュ読み取り操作が失敗した場合は NULL。

esp_ota_get_state_partition()
指定されたパーティションの状態を取得します。

引数説明
*partitionパーティションへのポインタ。
*ota_stateパーティションの状態。
戻り値ESP_OK: 成功。
ESP_ERR_INVALID_ARG: パーティションまたは ota_state 引数が NULL 。
ESP_ERR_NOT_SUPPORTED: パーティションは ota ではありません。
ESP_ERR_NOT_FOUND: パーティション テーブルに otadata がないか、指定されたパーティションの状態が見つかりませんでした。

esp_ota_mark_app_valid_cancel_rollback()
実行中のアプリが正常に動作していることを示すために呼び出されます。

引数説明
なし
戻り値ESP_OK: 成功。

esp_ota_mark_app_invalid_rollback_and_reboot()
以前動作していたアプリにロールバックするために呼び出されます。

引数説明
なし
戻り値ESP_FAIL: 成功しなかった場合。
ESP_ERR_OTA_ROLLBACK_FAILED: Flash にアプリがないため、ロールバックはできません。

パーティション設定

「ツール」→「Partition Scheme」から現在使用しているパーティション設定を確認してみます。

標準では「Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)」となっていると思います。

この構成は以下のファイルに記載されています。

default.csvArduino IDE インストール場所 →「C:\Users\xxxxxxxx\AppData\Local\Arduino15」)

Arduino IDE インストール場所\Arduino15\packages\esp32\hardware\esp32\1.0.6\tools\partitions\default.csv」

Arduino IDE インストール場所\Arduino15\packages\esp32\hardware\esp32\2.0.17\tools\partitions\default.csv」

<<1.0.6>>
# 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,

<<2.0.17>>
# 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,0x160000,
coredump, data, coredump,0x3F0000,0x10000,

app0 と app1 があるのがわかると思います。これがアプリを格納するパーティションであり、2つ用意されているため、ロールバックが可能となります。app0しかない場合は、パーティション設定を変更する必要があります。

<<補足>>
「Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)」と「default.csv」の紐づけは、「boards.txt」に記載されています。

boards.txtArduino IDE インストール場所 →「C:\Users\xxxxxxxx\AppData\Local\Arduino15」)

Arduino IDE インストール場所\Arduino15\packages\esp32\hardware\esp32\1.0.6\boards.txt」

Arduino IDE インストール場所\Arduino15\packages\esp32\hardware\esp32\2.0.17\boards.txt」

作業内容

スケッチ作成

「OTARollBack」というフォルダを作り、「OTARollBack.ino」というファイルを作成して、下記スケッチを作成しています。

配置例(「ファイル」→「基本設定」でスケッチブックの場所を確認)

スケッチブックの場所\OTARollBack\OTARollBack.ino」

<<OTARollBack.ino>>

#include <ArduinoOTA.h>
#include "esp_ota_ops.h"

const char* ssid = "xxxxxxxx";
const char* password = "xxxxxxxx";

extern "C" bool verifyOta(){
    ets_printf("HI IAM AN OVERRIDDEN FUNCTION!\n");
    return true;
}

void verifyFirmware(){
    Serial.printf("[SYSTEM] - Checking firmware...\n");
    const esp_partition_t *running = esp_ota_get_running_partition();
    esp_ota_img_states_t ota_state;
    if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
        const char* otaState = ota_state == ESP_OTA_IMG_NEW ? "ESP_OTA_IMG_NEW"
            : ota_state == ESP_OTA_IMG_PENDING_VERIFY ? "ESP_OTA_IMG_PENDING_VERIFY"
            : ota_state == ESP_OTA_IMG_VALID ? "ESP_OTA_IMG_VALID"
            : ota_state == ESP_OTA_IMG_INVALID ? "ESP_OTA_IMG_INVALID"
            : ota_state == ESP_OTA_IMG_ABORTED ? "ESP_OTA_IMG_ABORTED"
            : "ESP_OTA_IMG_UNDEFINED";
        Serial.printf( "[System] - Ota state: %s\n",otaState);

        if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
            if (esp_ota_mark_app_valid_cancel_rollback() == ESP_OK) {
                Serial.printf( "[System] - App is valid, rollback cancelled successfully\n");
            } else {
                Serial.printf("[System] - Failed to cancel rollback\n");
            }
        }
    }else{
        Serial.printf("[System] - OTA partition has no record in OTA data\n");
    }
}

void setup(){
    Serial.begin(115200);
    delay(10000);

    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();
    }

    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());

    Serial.println("ver. 1.0.0");

    verifyFirmware();
}

void loop(){
  ArduinoOTA.handle();
}

4行目と5行目の[ssid]と[password]の内容はご自身の環境に変更してください。

const char* ssid = "xxxxxxxx";
const char* password = "xxxxxxxx";

ESP-IDF sdkconfig 設定変更

sdkconfigの設定変更方法は以下を参照してください。
[ESP32]Arduino IDE で ESP-IDF の sdkconfig を変更する方法

[CONFIG_APP_ROLLBACK_ENABLE] を有効にします。

initArduino 修正

“Arduino IDE インストール場所”\packages\esp32\hardware\esp32\1.0.6\cores\esp32\
にある、[esp32-hal-misc.c] をエディタで開きます。

esp32-hal-misc.cArduino IDE インストール場所 →「C:\Users\xxxxxxxx\AppData\Local\Arduino15」)

Arduino IDE インストール場所\Arduino15\packages\esp32\hardware\esp32\1.0.6\cores\esp32\esp32-hal-misc.c

Arduino IDE インストール場所\Arduino15\packages\esp32\hardware\esp32\2.0.17\cores\esp32\esp32-hal-misc.c

void initArduino()
{
#ifdef CONFIG_APP_ROLLBACK_ENABLE
    const esp_partition_t *running = esp_ota_get_running_partition();
    esp_ota_img_states_t ota_state;
    if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
        if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
            if (verifyOta()) {
                esp_ota_mark_app_valid_cancel_rollback();
            } else {
                log_e("OTA verification failed! Start rollback to the previous version ...");
                esp_ota_mark_app_invalid_rollback_and_reboot();
            }
        }
    }
#endif

1.0.6の場合は180行目付近、2.0.17の場合は230行目付近に上記のコードがあります。

この処理は、verifyOta() の結果によりロールバックの実施を決定します。
verifyOta() は、初期動作は何もせずに true を返しますが、アプリ側でオーバーライドして使用します。
「OTARollBack.ino」の7行目がアプリ側で作成している内容です。

extern "C" bool verifyOta(){
  ets_printf("HI IAM AN OVERRIDDEN FUNCTION!\n");
  return true;
}


initArduino() は、アプリの setup() が呼び出される前に動作します。そのため、verifyOta() の中身はアプリ署名を使用した不正アプリの判定などを実施するのに使用できます。
今回は、そのような利用をしないため、オーバーライドした中身は常時 true を返すようにしておきます。

ただし、initArduino() で、verifyOta() が true を返した場合に、更新アプリが正常であることを確定させてしまっています。最終確定はアプリ内部でしたいので、ここはコメントアウトします。

            if (verifyOta()) {
//                esp_ota_mark_app_valid_cancel_rollback();
            } else {

動作確認

まず、用意したスケッチをシリアルポート接続して普通に書き込みます。

すると、状態が「ESP_OTA_IMG_UNDEFINED」であることが確認できました。UNDEFINED なので、ロールバックされません。

次に、81行目のバージョン情報を(ver. 1.0.1)にして、wifi経由(OTA)で書込みします。
この時、起動時のログをすぐに確認したいため別アプリのシリアルモニアを起動しておいた方がわかりやすいです。
私の場合は Arduino IDE 1.8.19 を起動して、シリアルモニタを表示させました。

すると、(ver. 1.0.1)が表示され、状態が「ESP_OTA_IMG_PENDING_VERIFY」で起動されました。そして、26行目の esp_ota_mark_app_valid_cancel_rollback() が呼ばれて、アプリ正常が確定しました。状態は表示していませんが、「ESP_OTA_IMG_VALID」になっているはずです。

この状態で、ESP32のリセットボタンを押してみてください。

先ほど、アプリ正常を確定させていますので、「ESP_OTA_IMG_VALID」で(ver. 1.0.1)が起動しました。

次は、ロールバックさせてみます。
26行目から30行目の処理をコメントアウトして、アプリ正常確定させなくします。
そして、(ver. 1.0.2)にしてOTA書込みします。

//            if (esp_ota_mark_app_valid_cancel_rollback() == ESP_OK) {
//                Serial.printf( "[System] - App is valid, rollback cancelled successfully\n");
//            } else {
//                Serial.printf("[System] - Failed to cancel rollback\n");
//            }

(ver. 1.0.2)が「ESP_OTA_IMG_PENDING_VERIFY」の状態で起動しました。
この状態で、ESP32のリセットボタンを押して、再起動します。アプリ正常確定していないのでロールバックするはずです。

意図した通りに、(ver. 1.0.1)にロールバックしました。

おわりに

なかなか便利ですね。少なくともOTAが起動し、次のアプリをアップロードする時間分経過した後にアプリ正常確定しておけば、アプリ異常のせいでクラッシュするような場合に設置場所まで行かなくて良さそうです。

コメント

タイトルとURLをコピーしました