無線アップデート

無線アップデート

無線アップデート(OTA)

ESP32で無線アップデートをやってみます。
ESP-IDFではWi-Fi経由かBluetooth経由でのアップデートが可能なようです。

Wi-Fi経由でしか試してませんので以下はWi-Fiでの無線アップデートの手順になります。
無線アップデート(Over The Air Updates)略してOTAになります。
公式のリンクは以下になります。

OTAはWebサーバからbinファイルをダウンロードしてアップデートします。
Webサーバを用意するのが面倒なのでGithubのリポジトリにbinファイルを置いてOTAを実行します。
以下が手順になります。

1.Webサーバのサーバー証明書を取得

httpsからダウンロードするのでTLSサーバー証明書を取得します。
以下のコマンドを実行します。

openssl s_client -connect github.com:443

情報がたくさん出てきますが、以下の部分だけ切り取ってca_cert.pemファイルに保存します。

-----BEGIN CERTIFICATE-----
MIIEozCCBEmgAwIBAgIQTij3hrZsGjuULNLEDrdCpTAKBggqhkjOPQQDAjCBjzEL
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
BxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5T
ZWN0aWdvIEVDQyBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMB4X
DTI0MDMwNzAwMDAwMFoXDTI1MDMwNzIzNTk1OVowFTETMBEGA1UEAxMKZ2l0aHVi
LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABARO/Ho9XdkY1qh9mAgjOUkW
mXTb05jgRulKciMVBuKB3ZHexvCdyoiCRHEMBfFXoZhWkQVMogNLo/lW215X3pGj
ggL+MIIC+jAfBgNVHSMEGDAWgBT2hQo7EYbhBH0Oqgss0u7MZHt7rjAdBgNVHQ4E
FgQUO2g/NDr1RzTK76ZOPZq9Xm56zJ8wDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB
/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEkGA1UdIARCMEAw
NAYLKwYBBAGyMQECAgcwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNv
bS9DUFMwCAYGZ4EMAQIBMIGEBggrBgEFBQcBAQR4MHYwTwYIKwYBBQUHMAKGQ2h0
dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb0VDQ0RvbWFpblZhbGlkYXRpb25T
ZWN1cmVTZXJ2ZXJDQS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3Rp
Z28uY29tMIIBgAYKKwYBBAHWeQIEAgSCAXAEggFsAWoAdwDPEVbu1S58r/OHW9lp
LpvpGnFnSrAX7KwB0lt3zsw7CAAAAY4WOvAZAAAEAwBIMEYCIQD7oNz/2oO8VGaW
WrqrsBQBzQH0hRhMLm11oeMpg1fNawIhAKWc0q7Z+mxDVYV/6ov7f/i0H/aAcHSC
Ii/QJcECraOpAHYAouMK5EXvva2bfjjtR2d3U9eCW4SU1yteGyzEuVCkR+cAAAGO
Fjrv+AAABAMARzBFAiEAyupEIVAMk0c8BVVpF0QbisfoEwy5xJQKQOe8EvMU4W8C
IGAIIuzjxBFlHpkqcsa7UZy24y/B6xZnktUw/Ne5q5hCAHcATnWjJ1yaEMM4W2zU
3z9S6x3w4I4bjWnAsfpksWKaOd8AAAGOFjrv9wAABAMASDBGAiEA+8OvQzpgRf31
uLBsCE8ktCUfvsiRT7zWSqeXliA09TUCIQDcB7Xn97aEDMBKXIbdm5KZ9GjvRyoF
9skD5/4GneoMWzAlBgNVHREEHjAcggpnaXRodWIuY29tgg53d3cuZ2l0aHViLmNv
bTAKBggqhkjOPQQDAgNIADBFAiEAru2McPr0eNwcWNuDEY0a/rGzXRfRrm+6XfZe
SzhYZewCIBq4TUEBCgapv7xvAtRKdVdi/b4m36Uyej1ggyJsiesA
-----END CERTIFICATE-----

ファイルはプロジェクトの main/certs/ca_cert.pem に配置するのが一般的のようです。

2.パーティションテーブル設定

OTAを利用する場合はOTA専用のパーティションが必要です。
OTAパーティションは実行用と書込用で最低2つ同じサイズのものが必要です。
手持ちのESP32がDevKitの物でFlashが4MBしかないのでカスタムパーティションを作成する必要があります。
プロジェクトフォルダにpartition.csvを作成します。
中身は以下のように設定します。

# Name,   Type, SubType,  Offset,   Size,  Flags
nvs,      data, nvs,      0x9000,  0x4000
otadata,  data, ota,      0xd000,  0x2000
phy_init, data, phy,      0xf000,  0x1000
ota_0,    app,  ota_0,    0x10000, 0x180000
ota_1,    app,  ota_1,    ,        0x180000
nvs_key,  data, nvs_keys, ,        0x1000

OTAパーティションは1.5MB取れるようにしてます。

作成したサーバ証明書を登録します。

大項目 項目 設定内容
Serial flasher config Flash size 4 MB
Partition Table Partition Table Custom partition table CSV
Custom partition CSV file partitions.csv
ESP-TLS Choose SSL/TLS library for ESP-TLS mbedTLS
Certificate Bundle Add custom certificates to thedefault bundle true
Custom certificate bundle path main/certs/ca_cert.pem

4.プログラム

単純に無線アップデートするだけなら以下のコードだけで動きます。
スイッチなどを使用して手動でアップデートする場合はこれでいいと思います。

const char* updateuri = "https://github.com/xxxxxxx/xxxxx/raw/xxxxx.bin";

esp_http_client_config_t config = {
    .url = updateuri,
    .crt_bundle_attach = esp_crt_bundle_attach,
    .keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
     .http_config = &config,
};
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
    esp_restart();
} else {
    ESP_LOGW(TAG, "OTA failed...");        
}

5.プログラム2

起動時に自動的にバージョンをチェックして新しいファームウェアがあったらアップデートする・・・みたいな仕様の場合はちょっと面倒です。

5-1.バージョン

まず作成したbinファイルにバージョンを埋め込みます。
binファイルには最大32byteのバーション文字列を埋め込むことができます。
binファイルのヘッダはesp_app_desc_t構造体になっています。
バージョンを埋め込む方法ですが以下の4通りの方法があります。

項目 説明
menuconfig Application manager
 Get the project version from Kconfig にチェック
 Project version にバージョン文字列設定
CMakeLists.txt 以下のようにPROJECT_VERを設定
 cmake_minimum_required(VERSION 3.5)
 set(PROJECT_VER “1.0.0”)
 include($ENV{IDF_PATH}/tools/cmake/project.cmake)
 project(xxxxx)
version.txt プロジェクトフォルダにversion.txtを作成
中身は例えば 1.0.0 のような文字列のみ
Git Gitリポジトリの場合はGit describeで取得できるタグがバージョンになります

上記以外ではモレなくバージョンは “1” になります。
一番分かり易いので version.txt を作る方法でよいかと思います。

5-2. プログラム
binに埋め込まれてるバージョンをチェックして、現在稼働中のバージョンと違う場合にアップデートを実行するようにします。

const buffsize = 1024;
char ota_write_data[buffsize + 1] = { 0 };
   const char* updateuri = pThis->m_configMap["updateuri"].c_str();
   ESP_LOGI(TAG, "update uri = %s", updateuri);
   
   esp_http_client_config_t config = {
       .url = updateuri,
       .crt_bundle_attach = esp_crt_bundle_attach,
       .keep_alive_enable = true,
   };
   esp_err_t err;
   esp_ota_handle_t update_handle = 0 ;
   const esp_partition_t *update_partition = NULL;
   const esp_partition_t *running = esp_ota_get_running_partition();
   esp_http_client_handle_t client = esp_http_client_init(&config);
   if (client == NULL) {
       ESP_LOGE(TAG, "Failed to initialise HTTP connection");
       ESP_ERROR_CHECK(ESP_FAIL);
   }
   ESP_ERROR_CHECK(esp_http_client_open(client, 0));
   esp_http_client_fetch_headers(client);

   update_partition = esp_ota_get_next_update_partition(NULL);
   if (update_partition == NULL) {
       ESP_LOGE(TAG, "update partition NULL");
       ESP_ERROR_CHECK(ESP_FAIL);
   }

   int binary_file_length = 0;
   bool image_header_was_checked = false;
   while(1) {
       int data_read = esp_http_client_read(client, ota_write_data, buffsize);
       int http_status = esp_http_client_get_status_code(client);
       if (http_status == 302) {
           // リダイレクト
           esp_http_client_set_redirection(client);
           ESP_ERROR_CHECK(esp_http_client_open(client, 0));
           esp_http_client_fetch_headers(client);
           continue;
       }
       if (data_read < 0) {
           ESP_LOGE(TAG, "Error: SSL data read error");
           esp_http_client_close(client);
           esp_http_client_cleanup(client);
       } else if (data_read > 0) {
           if (image_header_was_checked == false) {
               esp_app_desc_t new_app_info;
               if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) {
                   memcpy(&new_app_info, &ota_write_data[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t));
                   ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version);

                   esp_app_desc_t running_app_info;
                   if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) {
                       ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version);
                   }

                   const esp_partition_t* last_invalid_app = esp_ota_get_last_invalid_partition();
                   esp_app_desc_t invalid_app_info;
                   if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK) {
                       ESP_LOGI(TAG, "Last invalid firmware version: %s", invalid_app_info.version);
                   }

                   // check current version with last invalid partition
                   if (last_invalid_app != NULL) {
                       if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) {
                           ESP_LOGW(TAG, "New version is the same as invalid version.");
                           ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version);
                           ESP_LOGW(TAG, "The firmware has been rolled back to the previous version.");
                           esp_http_client_close(client);
                           esp_http_client_cleanup(client);
						return;
                       }
                   }

                   if (memcmp(new_app_info.version, running_app_info.version, sizeof(new_app_info.version)) == 0) {
                       ESP_LOGW(TAG, "Current running version is the same as a new. We will not continue the update.");
                       esp_http_client_close(client);
                       esp_http_client_cleanup(client);
					return;
                   }

                   image_header_was_checked = true;

                   err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle);
                   if (err != ESP_OK) {
                       ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
                       esp_http_client_close(client);
                       esp_http_client_cleanup(client);
                       esp_ota_abort(update_handle);
                       ESP_ERROR_CHECK(ESP_FAIL);
                   }
                   ESP_LOGI(TAG, "esp_ota_begin succeeded");                    
               } else {
                   ESP_LOGE(TAG, "received package is not fit len");
                   esp_http_client_close(client);
                   esp_http_client_cleanup(client);
                   ESP_ERROR_CHECK(ESP_FAIL);
               }
           }
           err = esp_ota_write( update_handle, (const void *)ota_write_data, data_read);
           if (err != ESP_OK) {
               esp_http_client_close(client);
               esp_http_client_cleanup(client);
               esp_ota_abort(update_handle);
               ESP_ERROR_CHECK(ESP_FAIL);
           }
           binary_file_length += data_read;
           ESP_LOGD(TAG, "Written image length %d", binary_file_length);
       } else if (data_read == 0) {
           if (errno == ECONNRESET || errno == ENOTCONN) {
               ESP_LOGE(TAG, "Connection closed, errno = %d", errno);
               break;
           }
           if (esp_http_client_is_complete_data_received(client) == true) {
               ESP_LOGI(TAG, "Connection closed");
               break;
           }            
       }
   }
   ESP_LOGI(TAG, "Total Write binary data length: %d", binary_file_length);
   if (esp_http_client_is_complete_data_received(client) != true) {
       ESP_LOGE(TAG, "Error in receiving complete file");
       esp_http_client_close(client);
       esp_http_client_cleanup(client);
       esp_ota_abort(update_handle);
       ESP_ERROR_CHECK(ESP_FAIL);
   }

   err = esp_ota_end(update_handle);
   if (err != ESP_OK) {
       if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
           ESP_LOGE(TAG, "Image validation failed, image is corrupted");
       } else {
           ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));
       }
       esp_http_client_close(client);
       esp_http_client_cleanup(client);
       ESP_ERROR_CHECK(ESP_FAIL);
   }

   err = esp_ota_set_boot_partition(update_partition);
   if (err != ESP_OK) {
       ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err));
       esp_http_client_close(client);
       esp_http_client_cleanup(client);
       ESP_ERROR_CHECK(ESP_FAIL);
   }
   ESP_LOGI(TAG, "Prepare to restart system!");
   esp_restart();

ちょっと長いですが、Webサーバーのbinファイルを読み込み、ヘッダ部のバージョンを取得して同じならアップデートをキャンセルしているだけです。


これでOTAはうまく動きました。
ただDevKitのFlash 4 MBだとかなりキツい。16 MB版がほしくなります。

コメント

このブログの人気の投稿

(真)ESP32の開発環境をVSCodeで作る

ESP32にmicroSD接続

ESP32でデバッグ