diff --git a/.idf_build_apps.toml b/.idf_build_apps.toml index 833a709ef1..dfef710636 100644 --- a/.idf_build_apps.toml +++ b/.idf_build_apps.toml @@ -8,6 +8,7 @@ manifest_file = [ "ccomp_timer/.build-test-rules.yml", "coremark/.build-test-rules.yml", "esp_daylight/.build-test-rules.yml", + "esp_delta_ota/.build-test-rules.yml", "esp_cli_commands/.build-test-rules.yml", "esp_encrypted_img/.build-test-rules.yml", "esp_flash_dispatcher/.build-test-rules.yml", diff --git a/esp_delta_ota/.build-test-rules.yml b/esp_delta_ota/.build-test-rules.yml new file mode 100644 index 0000000000..f9eff586ab --- /dev/null +++ b/esp_delta_ota/.build-test-rules.yml @@ -0,0 +1,9 @@ +esp_delta_ota/examples: + enable: + - if: IDF_TARGET == "esp32" + reason: Delta OTA example currently only tested on ESP32 + +esp_delta_ota/test_apps: + enable: + - if: IDF_TARGET == "esp32" + reason: Delta OTA test app currently only tested on ESP32 diff --git a/esp_delta_ota/examples/https_delta_ota/CMakeLists.txt b/esp_delta_ota/examples/https_delta_ota/CMakeLists.txt index b7d8384e3f..3e21ef43ab 100644 --- a/esp_delta_ota/examples/https_delta_ota/CMakeLists.txt +++ b/esp_delta_ota/examples/https_delta_ota/CMakeLists.txt @@ -1,7 +1,7 @@ # The following lines of boilerplate have to be in your project's CMakeLists # in this exact order for cmake to work correctly -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -set(COMPONENTS main) +set(COMPONENTS main esp_eth) project(https_delta_ota) diff --git a/esp_delta_ota/examples/https_delta_ota/main/CMakeLists.txt b/esp_delta_ota/examples/https_delta_ota/main/CMakeLists.txt index 537ed5ccb2..9252164fbf 100644 --- a/esp_delta_ota/examples/https_delta_ota/main/CMakeLists.txt +++ b/esp_delta_ota/examples/https_delta_ota/main/CMakeLists.txt @@ -1,4 +1,16 @@ -idf_component_register(SRCS "main.c" - INCLUDE_DIRS "." - EMBED_TXTFILES ca_cert.pem - PRIV_REQUIRES esp_http_client esp_partition nvs_flash app_update esp_timer esp_wifi console) +set(SRCS "main.c") +set(INCLUDE_DIRS ".") +set(EMBED_TXTFILES "tests/certs/servercert.pem") +set(PRIV_REQS "esp_http_client esp_partition nvs_flash app_update esp_timer esp_wifi console esp_eth esp_https_server") + +if(CONFIG_EXAMPLE_ENABLE_CI_TEST) + list(APPEND SRCS + "tests/test_local_server_ota.c") + list(APPEND INCLUDE_DIRS "tests") + list(APPEND EMBED_TXTFILES "tests/certs/prvtkey.pem") +endif() + +idf_component_register(SRCS ${SRCS} + INCLUDE_DIRS ${INCLUDE_DIRS} + EMBED_TXTFILES ${EMBED_TXTFILES} + PRIV_REQUIRES ${PRIV_REQS}) diff --git a/esp_delta_ota/examples/https_delta_ota/main/Kconfig.projbuild b/esp_delta_ota/examples/https_delta_ota/main/Kconfig.projbuild index d0499155e8..0625fa0121 100644 --- a/esp_delta_ota/examples/https_delta_ota/main/Kconfig.projbuild +++ b/esp_delta_ota/examples/https_delta_ota/main/Kconfig.projbuild @@ -24,4 +24,14 @@ menu "Example Configuration" help Maximum time for reception + config EXAMPLE_FIRMWARE_UPG_URL_FROM_STDIN + bool + default y if EXAMPLE_FIRMWARE_UPG_URL = "FROM_STDIN" + + config EXAMPLE_ENABLE_CI_TEST + bool "Enable the CI test code" + default n + help + This enables the CI test code i.e. https local server code. + endmenu diff --git a/esp_delta_ota/examples/https_delta_ota/main/main.c b/esp_delta_ota/examples/https_delta_ota/main/main.c index e90fdc6e2a..18a8d6c704 100644 --- a/esp_delta_ota/examples/https_delta_ota/main/main.c +++ b/esp_delta_ota/examples/https_delta_ota/main/main.c @@ -36,13 +36,18 @@ #define BUFFSIZE 1024 #define PATCH_HEADER_SIZE 64 #define DIGEST_SIZE 32 +#define OTA_URL_SIZE 256 static uint32_t esp_delta_ota_magic = 0xfccdde10; static const char *TAG = "https_delta_ota_example"; static char ota_write_data[BUFFSIZE + 1] = { 0 }; -extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start"); -extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end"); +extern const uint8_t server_cert_pem_start[] asm("_binary_servercert_pem_start"); +extern const uint8_t server_cert_pem_end[] asm("_binary_servercert_pem_end"); + +#ifdef CONFIG_EXAMPLE_ENABLE_CI_TEST +#include "test_local_server_ota.h" +#endif const esp_partition_t *current_partition, *destination_partition; static esp_ota_handle_t ota_handle; @@ -156,6 +161,29 @@ static void ota_example_task(void *pvParameter) config.skip_cert_common_name_check = true; #endif +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPG_URL_FROM_STDIN + if (strcmp(config.url, "FROM_STDIN") == 0) { + ESP_LOGI(TAG, "Reading OTA URL from stdin"); +#ifdef CONFIG_EXAMPLE_ENABLE_CI_TEST + delta_ota_test_firmware_data_from_stdin(&config.url); + if (config.url == NULL) { + ESP_LOGE(TAG, "Failed to read URL from stdin"); + abort(); + } +#else + char url_buf[OTA_URL_SIZE]; + example_configure_stdin_stdout(); + fgets(url_buf, OTA_URL_SIZE, stdin); + int len = strlen(url_buf); + url_buf[len - 1] = '\0'; + config.url = url_buf; +#endif + } else { + ESP_LOGE(TAG, "Configuration mismatch: wrong firmware upgrade image url"); + abort(); + } +#endif + esp_http_client_handle_t client = esp_http_client_init(&config); if (client == NULL) { ESP_LOGE(TAG, "Failed to initialise HTTP connection"); @@ -273,5 +301,12 @@ void app_main(void) * examples/protocols/README.md for more information about this function. */ ESP_ERROR_CHECK(example_connect()); + +#ifdef CONFIG_EXAMPLE_ENABLE_CI_TEST + /* Start the local HTTPS server for CI test */ + ESP_ERROR_CHECK(delta_ota_test_start_webserver()); + ESP_LOGI(TAG, "Local HTTPS server started for CI test"); +#endif + xTaskCreate(&ota_example_task, "ota_example_task", 8192, NULL, 5, NULL); } diff --git a/esp_delta_ota/examples/https_delta_ota/main/tests/certs/prvtkey.pem b/esp_delta_ota/examples/https_delta_ota/main/tests/certs/prvtkey.pem new file mode 100644 index 0000000000..20a4bdb624 --- /dev/null +++ b/esp_delta_ota/examples/https_delta_ota/main/tests/certs/prvtkey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhxF/y7bygndxP +wiWLSwS9LY3uBMaJgup0ufNKVhx+FhGQOu44SghuJAaH3KkPUnt6SOM8jC97/yQu +c32WukI7eBZoA12kargSnzdv5m5rZZpd+NznSSpoDArOAONKVlzr25A1+aZbix2m +KRbQS5w9o1N2BriQuSzd8gL0Y0zEk3VkOWXEL+0yFUT144HnErnD+xnJtHe11yPO +2fEzYaGiilh0ddL26PXTugXMZN/8fRVHP50P2OG0SvFpC7vghlLp4VFM1/r3UJnv +L6Oz3ALc6dhxZEKQucqlpj8l1UegszQToopemtIj0qXTHw2+uUnkUyWIPjPC+wdO +AoaprFTRAgMBAAECggEAE0HCxV/N1Q1h+1OeDDGL5+74yjKSFKyb/vTVcaPCrmaH +fPvp0ddOvMZJ4FDMAsiQS6/n4gQ7EKKEnYmwTqj4eUYW8yxGUn3f0YbPHbZT+Mkj +z5woi3nMKi/MxCGDQZX4Ow3xUQlITUqibsfWcFHis8c4mTqdh4qj7xJzehD2PVYF +gNHZsvVj6MltjBDAVwV1IlGoHjuElm6vuzkfX7phxcA1B4ZqdYY17yCXUnvui46z +Xn2kUTOOUCEgfgvGa9E+l4OtdXi5IxjaSraU+dlg2KsE4TpCuN2MEVkeR5Ms3Y7Q +jgJl8vlNFJDQpbFukLcYwG7rO5N5dQ6WWfVia/5XgQKBgQD74at/bXAPrh9NxPmz +i1oqCHMDoM9sz8xIMZLF9YVu3Jf8ux4xVpRSnNy5RU1gl7ZXbpdgeIQ4v04zy5aw +8T4tu9K3XnR3UXOy25AK0q+cnnxZg3kFQm+PhtOCKEFjPHrgo2MUfnj+EDddod7N +JQr9q5rEFbqHupFPpWlqCa3QmQKBgQDldWUGokNaEpmgHDMnHxiibXV5LQhzf8Rq +gJIQXb7R9EsTSXEvsDyqTBb7PHp2Ko7rZ5YQfyf8OogGGjGElnPoU/a+Jij1gVFv +kZ064uXAAISBkwHdcuobqc5EbG3ceyH46F+FBFhqM8KcbxJxx08objmh58+83InN +P9Qr25Xw+QKBgEGXMHuMWgQbSZeM1aFFhoMvlBO7yogBTKb4Ecpu9wI5e3Kan3Al +pZYltuyf+VhP6XG3IMBEYdoNJyYhu+nzyEdMg8CwXg+8LC7FMis/Ve+o7aS5scgG +1to/N9DK/swCsdTRdzmc/ZDbVC+TuVsebFBGYZTyO5KgqLpezqaIQrTxAoGALFCU +10glO9MVyl9H3clap5v+MQ3qcOv/EhaMnw6L2N6WVT481tnxjW4ujgzrFcE4YuxZ +hgwYu9TOCmeqopGwBvGYWLbj+C4mfSahOAs0FfXDoYazuIIGBpuv03UhbpB1Si4O +rJDfRnuCnVWyOTkl54gKJ2OusinhjztBjcrV1XkCgYEA3qNi4uBsPdyz9BZGb/3G +rOMSw0CaT4pEMTLZqURmDP/0hxvTk1polP7O/FYwxVuJnBb6mzDa0xpLFPTpIAnJ +YXB8xpXU69QVh+EBbemdJWOd+zp5UCfXvb2shAeG3Tn/Dz4cBBMEUutbzP+or0nG +vSXnRLaxQhooWm+IuX9SuBQ= +-----END PRIVATE KEY----- diff --git a/esp_delta_ota/examples/https_delta_ota/main/tests/certs/servercert.pem b/esp_delta_ota/examples/https_delta_ota/main/tests/certs/servercert.pem new file mode 100644 index 0000000000..b29ba7ab1f --- /dev/null +++ b/esp_delta_ota/examples/https_delta_ota/main/tests/certs/servercert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWDCCAkACCQCbF4+gVh/MLjANBgkqhkiG9w0BAQsFADBuMQswCQYDVQQGEwJJ +TjELMAkGA1UECAwCTUgxDDAKBgNVBAcMA1BVTjEMMAoGA1UECgwDRVNQMQwwCgYD +VQQLDANFU1AxDDAKBgNVBAMMA0VTUDEaMBgGCSqGSIb3DQEJARYLZXNwQGVzcC5j +b20wHhcNMjEwNzEyMTIzNjI3WhcNNDEwNzA3MTIzNjI3WjBuMQswCQYDVQQGEwJJ +TjELMAkGA1UECAwCTUgxDDAKBgNVBAcMA1BVTjEMMAoGA1UECgwDRVNQMQwwCgYD +VQQLDANFU1AxDDAKBgNVBAMMA0VTUDEaMBgGCSqGSIb3DQEJARYLZXNwQGVzcC5j +b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDhxF/y7bygndxPwiWL +SwS9LY3uBMaJgup0ufNKVhx+FhGQOu44SghuJAaH3KkPUnt6SOM8jC97/yQuc32W +ukI7eBZoA12kargSnzdv5m5rZZpd+NznSSpoDArOAONKVlzr25A1+aZbix2mKRbQ +S5w9o1N2BriQuSzd8gL0Y0zEk3VkOWXEL+0yFUT144HnErnD+xnJtHe11yPO2fEz +YaGiilh0ddL26PXTugXMZN/8fRVHP50P2OG0SvFpC7vghlLp4VFM1/r3UJnvL6Oz +3ALc6dhxZEKQucqlpj8l1UegszQToopemtIj0qXTHw2+uUnkUyWIPjPC+wdOAoap +rFTRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAItw24y565k3C/zENZlxyzto44ud +IYPQXN8Fa2pBlLe1zlSIyuaA/rWQ+i1daS8nPotkCbWZyf5N8DYaTE4B0OfvoUPk +B5uGDmbuk6akvlB5BGiYLfQjWHRsK9/4xjtIqN1H58yf3QNROuKsPAeywWS3Fn32 +3//OpbWaClQePx6udRYMqAitKR+QxL7/BKZQsX+UyShuq8hjphvXvk0BW8ONzuw9 +RcoORxM0FzySYjeQvm4LhzC/P3ZBhEq0xs55aL2a76SJhq5hJy7T/Xz6NFByvlrN +lFJJey33KFrAf5vnV9qcyWFIo7PYy2VsaaEjFeefr7q3sTFSMlJeadexW2Y= +-----END CERTIFICATE----- diff --git a/esp_delta_ota/examples/https_delta_ota/main/tests/hello_world_esp32.bin b/esp_delta_ota/examples/https_delta_ota/main/tests/hello_world_esp32.bin new file mode 100644 index 0000000000..1d4a0eb3a0 Binary files /dev/null and b/esp_delta_ota/examples/https_delta_ota/main/tests/hello_world_esp32.bin differ diff --git a/esp_delta_ota/examples/https_delta_ota/main/tests/test_local_server_ota.c b/esp_delta_ota/examples/https_delta_ota/main/tests/test_local_server_ota.c new file mode 100644 index 0000000000..13a3050f04 --- /dev/null +++ b/esp_delta_ota/examples/https_delta_ota/main/tests/test_local_server_ota.c @@ -0,0 +1,216 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* Delta OTA HTTPS example's test file + * + * This example code is in the Public Domain (or CC0 licensed, at your option.) + * + * Unless required by applicable law or agreed to in writing, this + * software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. + */ +#include "esp_https_server.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "test_local_server_ota.h" +#include "protocol_examples_common.h" +#include "esp_partition.h" +#include +#include +#include + +#define OTA_URL_SIZE 256 +#define PARTITION_READ_BUFFER_SIZE 256 +#define PARTITION_READ_SIZE PARTITION_READ_BUFFER_SIZE + +static const char *TAG = "test_local_server_ota"; +static size_t patch_size = 0; + +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPG_URL_FROM_STDIN +void delta_ota_test_firmware_data_from_stdin(const char **data) +{ + char input_buf[OTA_URL_SIZE]; + if (strcmp(*data, "FROM_STDIN") == 0) { + example_configure_stdin_stdout(); + fflush(stdin); + char *url = NULL; + char *tokens[OTA_URL_SIZE]; + char *saveptr; + int token_count = 0; + + if (fgets(input_buf, OTA_URL_SIZE, stdin) == NULL) { + ESP_LOGE(TAG, "Failed to read URL from stdin"); + abort(); + } + int len = strlen(input_buf); + if (len == 0) { + ESP_LOGE(TAG, "Empty URL read from stdin"); + abort(); + } + if (input_buf[len - 1] == '\n') { + input_buf[len - 1] = '\0'; + len--; + } + char *token = strtok_r(input_buf, " ", &saveptr); + if (token == NULL) { + ESP_LOGE(TAG, "No URL token found in input"); + return; + } + // First token is the URL + url = token; + tokens[token_count++] = url; + // Process remaining tokens + while ((token = strtok_r(NULL, " ", &saveptr)) != NULL) { + tokens[token_count++] = token; + } + // Require patch_size to be provided (at least 2 tokens: URL and patch_size) + if (token_count < 2) { + ESP_LOGE(TAG, "Expected URL and patch_size, but only got %d token(s)", token_count); + return; + } + *data = strdup(tokens[0]); + // Assign the URL and additional data after the loop + if (token_count > 1) { + ESP_LOGI(TAG, "patch_size: %s\n", tokens[1]); + patch_size = atoi(tokens[1]); // Assuming the next token is the patch size + } + // Tokens are collected in the tokens array + } else { + ESP_LOGE(TAG, "Configuration mismatch: wrong firmware upgrade image url"); + abort(); + } +} +#endif + +/* An HTTP GET handler */ +static esp_err_t root_get_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/octet-stream"); + + // Find the patch_data partition where pytest writes the patch + const esp_partition_t *p = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, + ESP_PARTITION_SUBTYPE_ANY, "patch_data"); + + if (p == NULL) { + ESP_LOGE(TAG, "patch_data partition not found"); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Partition not found"); + return ESP_FAIL; + } + + if (patch_size == 0) { + ESP_LOGE(TAG, "Patch size is 0"); + return ESP_FAIL; + } + + int image_len = patch_size; + char buffer[PARTITION_READ_BUFFER_SIZE]; + int size = PARTITION_READ_SIZE; + int offset = 0; + + do { + /* Read file in chunks into the scratch buffer */ + if (offset + size > image_len) { + size = image_len - offset; + } + if (size == 0) { + break; + } + esp_err_t ret = esp_partition_read(p, offset, buffer, size); + if (ret == ESP_OK) { + /* Send the buffer contents as HTTP response chunk */ + if (httpd_resp_send_chunk(req, buffer, size) != ESP_OK) { + ESP_LOGE(TAG, "File sending failed!"); + /* Abort sending file */ + httpd_resp_sendstr_chunk(req, NULL); + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + return ESP_FAIL; + } + } else { + ESP_LOGE(TAG, "Partition read failed: %s", esp_err_to_name(ret)); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read partition"); + return ESP_FAIL; + } + offset += size; + + /* Keep looping till the whole file is sent */ + } while (offset < image_len); + + ESP_LOGI(TAG, "Patch file sending complete"); + + // Set headers + httpd_resp_set_hdr(req, "Accept-Ranges", "bytes"); + httpd_resp_set_hdr(req, "Connection", "close"); + httpd_resp_send_chunk(req, NULL, 0); + + return ESP_OK; +} + +static esp_err_t root_head_handler(httpd_req_t *req) +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, + ESP_PARTITION_SUBTYPE_ANY, "patch_data"); + + if (partition == NULL) { + ESP_LOGE(TAG, "Partition not found"); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Partition not found"); + return ESP_FAIL; + } + + if (patch_size == 0) { + return ESP_FAIL; + } + + // Get the size of the patch + httpd_resp_set_type(req, "application/octet-stream"); + httpd_resp_set_hdr(req, "Accept-Ranges", "bytes"); + httpd_resp_set_hdr(req, "Connection", "close"); + + // Complete HEAD response with no body + return httpd_resp_send(req, NULL, patch_size); // No body for HEAD method +} + +static const httpd_uri_t get_root = { + .uri = "/patch.bin", + .method = HTTP_GET, + .handler = root_get_handler +}; + +static const httpd_uri_t head_root = { + .uri = "/patch.bin", + .method = HTTP_HEAD, + .handler = root_head_handler +}; + +esp_err_t delta_ota_test_start_webserver(void) +{ + httpd_handle_t server = NULL; + // Start the httpd server + ESP_LOGI(TAG, "Starting HTTPS server for CI test"); + + httpd_ssl_config_t conf = HTTPD_SSL_CONFIG_DEFAULT(); + + extern const unsigned char servercert_start[] asm("_binary_servercert_pem_start"); + extern const unsigned char servercert_end[] asm("_binary_servercert_pem_end"); + conf.servercert = servercert_start; + conf.servercert_len = servercert_end - servercert_start; + + extern const unsigned char prvtkey_pem_start[] asm("_binary_prvtkey_pem_start"); + extern const unsigned char prvtkey_pem_end[] asm("_binary_prvtkey_pem_end"); + conf.prvtkey_pem = prvtkey_pem_start; + conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start; + + esp_err_t ret = httpd_ssl_start(&server, &conf); + if (ESP_OK != ret) { + ESP_LOGE(TAG, "Error starting server!"); + return ret; + } + + // Set URI handlers + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &get_root); + httpd_register_uri_handler(server, &head_root); + return ESP_OK; +} diff --git a/esp_delta_ota/examples/https_delta_ota/main/tests/test_local_server_ota.h b/esp_delta_ota/examples/https_delta_ota/main/tests/test_local_server_ota.h new file mode 100644 index 0000000000..9014cb1f59 --- /dev/null +++ b/esp_delta_ota/examples/https_delta_ota/main/tests/test_local_server_ota.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +#ifdef CONFIG_EXAMPLE_ENABLE_CI_TEST + +/** + * @brief starts the https server + * + * The server will serve the patch file from the patch_data partition. + * The patch_size must be set via delta_ota_test_firmware_data_from_stdin() + * before starting the server. NOTE - patch_size cannot be 0. + */ +esp_err_t delta_ota_test_start_webserver(void); + +/** + * @brief Takes the firmware URL from the STDIN (if want to send + * other data write the data in just one line by adding " " delimiter). + * + * @param data pointer to the firmware URL (or URL including other data) + */ +void delta_ota_test_firmware_data_from_stdin(const char **data); +#endif diff --git a/esp_delta_ota/examples/https_delta_ota/partitions.csv b/esp_delta_ota/examples/https_delta_ota/partitions.csv index a6dc2675a0..947f43e357 100644 --- a/esp_delta_ota/examples/https_delta_ota/partitions.csv +++ b/esp_delta_ota/examples/https_delta_ota/partitions.csv @@ -1,6 +1,7 @@ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 20K -otadata, data, ota, , 8K -phy_init, data, phy, , 4K -ota_0, app, ota_0, , 1280K -ota_1, app, ota_1, , 1280K +otadata, data, ota, 0xE000, 8K +phy_init, data, phy, 0x10000, 4K +ota_0, app, ota_0, 0x20000, 1280K +ota_1, app, ota_1, 0x160000, 1280K +patch_data, data, 0x40, 0x2A0000, 512K diff --git a/esp_delta_ota/examples/https_delta_ota/pytest_https_delta_ota.py b/esp_delta_ota/examples/https_delta_ota/pytest_https_delta_ota.py new file mode 100644 index 0000000000..f7a9f34028 --- /dev/null +++ b/esp_delta_ota/examples/https_delta_ota/pytest_https_delta_ota.py @@ -0,0 +1,225 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 + +import contextlib +import io +import os +import subprocess +import sys +import pexpect +from typing import Any + +import esptool +import pytest +from pytest_embedded import Dut + +PATCH_DATA_PARTITION_OFFSET = 0x2A0000 # Hardcoded offset for patch_data partition from partitions.csv + +def get_env_config_variable(env_name, var_name): + return os.environ.get(f'{env_name}_{var_name}'.upper()) + +def _ensure_requirements_installed(): + example_dir = os.path.dirname(os.path.abspath(__file__)) + requirements_path = os.path.join(example_dir, 'tools', 'requirements.txt') + + if not os.path.exists(requirements_path): + raise Exception(f'Requirements file not found at {requirements_path}') + + result = subprocess.run( + [sys.executable, '-m', 'pip', 'install', '-r', requirements_path], + cwd=os.path.dirname(requirements_path), + check=False, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + raise RuntimeError( + f'Failed to install requirements from {requirements_path} (exit code {result.returncode}):\n' + f'{result.stderr}' + ) + +def setting_connection(dut: Dut, env_name: str | None = None) -> Any: + if env_name is not None and dut.app.sdkconfig.get('EXAMPLE_WIFI_SSID_PWD_FROM_STDIN') is True: + dut.expect('Please input ssid password:') + ap_ssid = get_env_config_variable(env_name, 'ap_ssid') + ap_password = get_env_config_variable(env_name, 'ap_password') + dut.write(f'{ap_ssid} {ap_password}') + try: + ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=60)[1].decode() + print(f'Connected to AP/Ethernet with IP: {ip_address}') + except pexpect.exceptions.TIMEOUT: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet') + return ip_address + + +def find_hello_world_binary(base_dir, chip_target='esp32'): + """ + Find the pre-built hello_world binary for the target chip. + + This function looks for hello_world_.bin in the tests directory + under the given base directory (e.g., main/tests/). + These binaries are pre-built and checked into the repository for testing. + + Args: + base_dir: Base directory that contains the tests subdirectory (e.g., main/) + chip_target: Target chip (default: 'esp32') + + Returns: + Path to the hello_world binary file + """ + # Look for hello_world binary in tests directory + binary_name = f'hello_world_{chip_target}.bin' + binary_path = os.path.join(base_dir, 'tests', binary_name) + + if os.path.exists(binary_path): + return binary_path + + # Fallback: try generic hello_world.bin + fallback_path = os.path.join(example_dir, 'tests', 'hello_world.bin') + if os.path.exists(fallback_path): + print(f'Warning: Using generic hello_world.bin instead of {binary_name}') + return fallback_path + + raise Exception(f'Hello world binary not found at {binary_path}. ' + f'Expected pre-built binary: {binary_name} in tests/ directory. ' + f'Example dir: {example_dir}') + + +def generate_patch(base_binary, new_binary, patch_output, chip='esp32'): + """Generate delta OTA patch using the esp_delta_ota_patch_gen.py tool.""" + _ensure_requirements_installed() + + # Find the tool in the tools directory + example_dir = os.path.dirname(os.path.abspath(__file__)) + tool_path = os.path.join(example_dir, 'tools', 'esp_delta_ota_patch_gen.py') + + if not os.path.exists(tool_path): + raise Exception(f'Patch generation tool not found at {tool_path}') + + # Verify input files exist + if not os.path.exists(base_binary): + raise Exception(f'Base binary not found at {base_binary}') + if not os.path.exists(new_binary): + raise Exception(f'New binary not found at {new_binary}') + + # Use the tool to generate patch + cmd = [ + sys.executable, + tool_path, + 'create_patch', + '--chip', chip, + '--base_binary', base_binary, + '--new_binary', new_binary, + '--patch_file_name', patch_output + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + + # Print output + if result.stdout: + print(result.stdout) + if result.stderr: + print('STDERR:', result.stderr) + + if result.returncode != 0: + raise Exception(f'Patch generation failed with return code {result.returncode}') + + if not os.path.exists(patch_output): + raise Exception(f'Patch file not created at {patch_output}') + + print(f'Patch created successfully: {patch_output} ({os.path.getsize(patch_output)} bytes)') + +def write_patch_to_partition(dut: Dut, patch_file: str): + """Write the patch file to the patch_data partition on the device. + + Uses the existing esptool connection managed by pytest-embedded to avoid + serial port conflicts. The device is hard-reset after writing so it + boots fresh with the patch available on the partition. + """ + patch_size = os.path.getsize(patch_file) + + # Hardcoded offset for patch_data partition from partitions.csv + # OTA partitions must be aligned to 0x10000 boundaries + # Calculated as: phy_init ends at 0x11000, ota_0 aligned to 0x20000, ota_1 at 0x160000, patch_data at 0x2A0000 + offset = PATCH_DATA_PARTITION_OFFSET + print(f'Writing patch ({patch_size} bytes) to patch_data partition at offset {hex(offset)}') + + serial = dut.serial + + # Reuse the same pattern as EspSerial.use_esptool() decorator: + # 1. stop the serial redirect thread (releases the pyserial port) + # 2. let esptool reuse the existing connection + # 3. resume the redirect thread when done + # Use a local buffer instead of private attribute to avoid brittleness + with serial.disable_redirect_thread(): + esptool_output = io.StringIO() + with contextlib.redirect_stdout(esptool_output): + settings = serial.proc.get_settings() # Save the current serial settings + serial.esp.connect() # Connect to the device using esptool + esptool.main( + ['write-flash', hex(offset), patch_file], + esp=serial.esp, + ) + serial.proc.apply_settings(settings) # Restore the original serial settings + # Log esptool output for debugging + output = esptool_output.getvalue() + if output: + print(f'esptool output: {output}') + + print('Successfully wrote patch to patch_data partition') + + # Hard-reset so the device boots fresh (network + local server + stdin wait) + serial.hard_reset() + + +@pytest.mark.parametrize('target', ['esp32']) +@pytest.mark.ethernet +def test_esp_delta_ota(dut: Dut): + example_dir = os.path.dirname(os.path.abspath(__file__)) + build_dir = dut.app.binary_path + chip_target = getattr(dut, 'target', None) or os.environ.get('IDF_TARGET', 'esp32') + + try: + # Step 1: Get base and new binaries + base_binary = os.path.join(build_dir, 'https_delta_ota.bin') + if not os.path.exists(base_binary): + raise Exception(f'Base binary not found at {base_binary}. Device was flashed from build directory: {build_dir}') + + # Use find_hello_world_binary helper to locate the test binary + new_binary = find_hello_world_binary(os.path.join(example_dir, 'main'), chip_target) + + # Step 2: Generate patch + patch_file = os.path.join(build_dir, 'patch.bin') + generate_patch(base_binary, new_binary, patch_file, chip_target) + + # Step 3: Write patch to the patch_data partition + write_patch_to_partition(dut, patch_file) + + # Step 4: Connect device and get IP + env_name = 'wifi_high_traffic' if dut.app.sdkconfig.get('EXAMPLE_WIFI_SSID_PWD_FROM_STDIN') is True else None + device_ip = setting_connection(dut, env_name) + print(f'Device connected with IP: {device_ip}') + + # Step 5: Wait for local server to start + dut.expect('Local HTTPS server started for CI test', timeout=30) + print('Local HTTPS server started on device') + + # Step 6: Provide OTA URL to device (using device's own IP) + patch_size = os.path.getsize(patch_file) + ota_url = f'https://{device_ip}:443/patch.bin' + print(f'Providing OTA URL to device: {ota_url} {patch_size}') + dut.expect('Reading OTA URL from stdin', timeout=60) + dut.write(f'{ota_url} {patch_size}\n') + + # Step 7: Wait for OTA to start and complete + dut.expect('Rebooting in', timeout=90) # Device preparing to reboot + + # Step 8: Wait for reboot and new firmware to boot + dut.expect('Hello world!', timeout=60) + + print('Delta OTA test PASSED: Successfully updated from https_delta_ota to hello_world') + + except Exception as e: + print(f'HTTPS Delta OTA test FAILED: {str(e)}') + raise diff --git a/esp_delta_ota/examples/https_delta_ota/sdkconfig.ci b/esp_delta_ota/examples/https_delta_ota/sdkconfig.ci new file mode 100644 index 0000000000..97d4ce74c7 --- /dev/null +++ b/esp_delta_ota/examples/https_delta_ota/sdkconfig.ci @@ -0,0 +1,10 @@ +CONFIG_EXAMPLE_FIRMWARE_UPG_URL="FROM_STDIN" +CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_EXAMPLE_ENABLE_CI_TEST=y + +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_CONNECT_WIFI=n + +CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y +CONFIG_ESP_HTTPS_SERVER_ENABLE=y diff --git a/esp_delta_ota/examples/https_delta_ota/tools/requirements.txt b/esp_delta_ota/examples/https_delta_ota/tools/requirements.txt index fb628eadfb..de540d2ed8 100644 --- a/esp_delta_ota/examples/https_delta_ota/tools/requirements.txt +++ b/esp_delta_ota/examples/https_delta_ota/tools/requirements.txt @@ -1 +1,2 @@ detools>=0.49.0 +esptool