無線アップデート
無線アップデート(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取れるようにしてます。
3.menuconfigの設定
作成したサーバ証明書を登録します。
大項目 | 項目 | 設定内容 |
---|---|---|
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版がほしくなります。
コメント
コメントを投稿