🙆‍♀️

【ESP32】ハードウェアトリガーでファームウェアを初期化する方法

2024/05/11に公開

はじめに

方言を話す、おしゃべり猫型ロボット「ミーア」を開発中。

前回、こちらの記事で、ESP32のOTAアップデート機能を実装した。

https://kazulog.fun/dev/esp32-ota-update-vol1/

今回は、ファームウェアの初期化を実装したいと思う。ユーザーの長期間にわたる操作や予期しないエラーなどで、ESP32のファイルシステムが壊れた際に、ユーザー手動でファームウェアを初期化できるようにするため。

PlatformIOとTFT_eSPIを活用して、セーフモードでのディスプレイを利用した状態表示についても記載。

初期化のトリガーをどうするか

初期化のトリガーに関して、今回は以下の2つが考えられる。

MQTT経由での初期化

アプリケーションやクラウドサーバーからMQTTメッセージを送信し、そのメッセージを受け取ったESP32が初期化を行う。この方法の利点は、遠隔地からでもデバイスをリセットできること。しかし、ネット環境が良くないと途中でリセット操作が中断する可能性がある。

ハードウェアトリガー

物理的なボタンやタッチセンサーを使ってユーザーが特定の操作(例えば長押し)をすると初期化を行う方法。この方法の利点は、物理的なアクションが必要なため、誤って初期化が行われるリスクが低い。

ちなみに、Nintendo Switchは、電源ボタンを12秒以上押し続けることを、デバイスリセットとしている。

https://support-jp.nintendo.com/app/answers/detail/a_id/33800/~/【switch】本体の電源がonになりません。どうすればよいですか?

ミーアの頭にTTP223タッチセンサーを設置しているので、今回は、ハードウェアトリガーにして、頭を5秒以上タッチし続けた場合に初期化するという方法にしたいと思う。

Espressifが推奨しているESP32の初期化は、OTAアップデート用の2つのpartitionとは別に、初期化用のfactory partitionをOTAアップデートと同じサイズであらかじめ確保しておき、そこに初期化用のデータを保持しておき、デバイスリセット時はfactoryパーティションから初期化データを読み取る方法。

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/partition-tables.html

ただし、今回は、SPIFFS領域をある程度確保しなければならず、factoryパーティションを新たに用意するスペースがない。ということで、ハードウェアトリガーではあるが、初期ファームウェアバージョン(1.0.0)を指定してOTAアップデート関数を呼び出す方法で初期化を行いたいと思う。

実装ステップ

セーフモードのトリガー条件の追加

**setup()**の冒頭で、TTP223タッチセンサーがタッチされている時間を計測し、指定した時間(5秒)以上タッチされた場合に、セーフモードを実行する。

セーフモードでは、システムの基本的な機能のみを起動し、ネットワークやデータベースの初期化などは行わないようにする。

ファームウェアバージョンの指定

タッチが長時間継続されたことを検出したら、初期ファームウェアバージョン(1.0.0)を指定してOTAアップデート関数を呼び出す。

OTAアップデートの実行

指定されたバージョンのファームウェアをダウンロードし、インストールする。

それでは、この順に開発していきたいと思う。

setup()関数でセーフモードを分離

電源を押して起動した時に、TTP223タッチセンサーを触ったままで5秒以上が経過すると、セーフモードをアクティブにして、通常の初期化処理はスキップするようにする。

フラグ inSafeMode は、セーフモードがアクティブかどうかを示すために使用。セーフモードがトリガーされる場面では、このフラグを true に設定する。

// src/main.cpp
bool inSafeMode = false;

void setup() {
  Serial.begin(115200);
  pinMode(HEAD_BUTTON_PIN, INPUT);
  Serial.println("Starting");
  print_free_heap_size("After Starting");

  // セーフモードのトリガーをチェック
  unsigned long startTime = millis();
  while (digitalRead(HEAD_BUTTON_PIN) == HIGH) {
    if (millis() - startTime > 5000) {  // 5秒以上押されていたらセーフモード
      Serial.println("Entering Safe Mode...");
      setupSafeMode();
      return;
    }
  }

  setupCommon();
  normalSetup();
}

共通セットアップ: セーフモードと通常モード

セーフモードにおいても、デバイスの基本的な機能を保持し、Wi-Fi接続を通じてのOTAアップデートを実行できるようにするために、NVSとLittleFSの初期化、そしてWi-Fiの設定を共通関数に組み込む。

共通セットアップ以外のセットアップ機能(デバイスシャドウ初期化・DB初期化・音声初期化など)はnotmalSetup()関数として、セーフモード時には起動しないように変更。

// src/main.cpp
void setupCommon() {
    // NVSの初期化
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        esp_err_t erase_err = nvs_flash_erase();
        if (erase_err != ESP_OK) {
            Serial.println("Failed to erase NVS partition");
            return;
        }
        err = nvs_flash_init();
    }
    if (err != ESP_OK) {
        Serial.println("Failed to initialize NVS");
        return;
    }

    // LittleFSの初期化
    if (!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) {
        Serial.println("Failed to mount file system");
        return;
    }
    // Wi-Fi接続
    setupWiFi(ssid, password);
}

Wi-Fi対応:SmartConfig設定

セーフモードで起動中に、Wi-Fiが未接続の場合、Wi-Fi接続を行う必要があるので、SmartConfigを使用してWi-Fi接続の設定を行う。

元々、Bluetoothを用いて、アプリとESP32のWi-Fi接続を行っていたが、Bluetoothを利用した製品を販売する場合、Bluetooth SIG(Special Interest Groupの略)認証を取得しなければならず、費用が8,000ドルもかかることが判明したために、急遽SmartConfigを利用したWi-Fi接続へと変更した。

// src/main.cpp
void setupSafeMode() {
  Serial.println("Entering Safe Mode: Limited functionality.");
  inSafeMode = true;

  // 共通の初期化処理を行う
  setupCommon();

  // WiFiが接続されていないか確認
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Starting SmartConfig to setup WiFi...");
    WiFi.disconnect();
    WiFi.mode(WIFI_STA);
    WiFi.beginSmartConfig();

    // SmartConfigの完了を待つ
    while (!WiFi.smartConfigDone()) {
      delay(500);
      Serial.print(".");
    }
    Serial.println("\nSmartConfig Done.");

    // WiFi接続の確立を待つ
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
    }
    Serial.println("\nWiFi Connected.");
  } else {
    Serial.println("WiFi is already connected.");
  }
}

loop()関数でをセーフモード分離

loop() 関数では、セーフモードがアクティブかどうかをチェックし、アクティブであれば通常のループ処理をスキップして safeModeLoop() を実行する。

// src/main.cpp
void loop() {
  if (inSafeMode) {
    safeModeLoop();
    return;
  }

  // 通常の処理
  monitorWiFiConnectionChange();
  if (isWiFiConnected()) {
    executeWiFiConnectedRoutines();
  }
  buttonManager.handleButtonPress();
  ExpressionService::getInstance().render();
  delay(10);
}

OTAで初期化実行

safeModeLoop() では、OTAアップデートを行い、ファームウェアのアップデートを通じてデバイスを初期化する。初期化が

Discussion