From f7646ba3c7deee329634455c908b6ae9fb6d5156 Mon Sep 17 00:00:00 2001 From: Shreyash Bubane Date: Fri, 4 Oct 2024 17:17:39 +0530 Subject: [PATCH 1/4] change function signature for ota_report functions and support custom report functions for default OTA callback --- .../esp_rainmaker/include/esp_rmaker_ota.h | 15 +++++++ .../esp_rainmaker/src/ota/esp_rmaker_ota.c | 45 ++++++++++++++++--- .../src/ota/esp_rmaker_ota_internal.h | 5 ++- .../src/ota/esp_rmaker_ota_using_params.c | 3 +- .../src/ota/esp_rmaker_ota_using_topics.c | 13 +++--- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/components/esp_rainmaker/include/esp_rmaker_ota.h b/components/esp_rainmaker/include/esp_rmaker_ota.h index e7a93552..5c5e2413 100644 --- a/components/esp_rainmaker/include/esp_rmaker_ota.h +++ b/components/esp_rainmaker/include/esp_rmaker_ota.h @@ -65,6 +65,19 @@ typedef enum { /** The OTA Handle to be used by the OTA callback */ typedef void *esp_rmaker_ota_handle_t; +/** Function Prototype for Reporting Intermediate OTA States + * + * This function is called to notify RainMaker dashbord of OTA Progress + * + * @param ota_job_id Job Id to report. + * @param status OTA status to report. + * @param additional_info Descriptionn to report. + * + * @return ESP_OK on success + * @return error on faliure + */ +typedef esp_err_t (*esp_rmaker_ota_report_fn_t)(char *ota_job_id, ota_status_t status, char* additional_info); + /** OTA Data */ typedef struct { @@ -83,6 +96,8 @@ typedef struct { char *priv; /** OTA Metadata. Applicable only for OTA using Topics. Will be received (if applicable) from the backend, along with the OTA URL */ char *metadata; + /** The Function to be called for reporting OTA status. This can be used if needed to override transport of OTA*/ + esp_rmaker_ota_report_fn_t report_fn; } esp_rmaker_ota_data_t; /** Function prototype for OTA Callback diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c index 0982bb4a..bf570a24 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota.c +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c @@ -122,10 +122,16 @@ esp_err_t esp_rmaker_ota_report_status(esp_rmaker_ota_handle_t ota_handle, ota_s } esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; esp_err_t err = ESP_FAIL; - if (ota->type == OTA_USING_PARAMS) { - err = esp_rmaker_ota_report_status_using_params(ota_handle, status, additional_info); - } else if (ota->type == OTA_USING_TOPICS) { - err = esp_rmaker_ota_report_status_using_topics(ota_handle, status, additional_info); + + if (ota->report_fn) { + char *job_id = NULL; + if (ota->transient_priv) { + job_id = ota->transient_priv; + } + err = ota->report_fn(job_id, status, additional_info); + + } else { + ESP_LOGE(TAG, "Report fn not found"); } if (err == ESP_OK) { esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; @@ -337,6 +343,24 @@ esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmak if (!ota_data->url) { return ESP_FAIL; } + + esp_rmaker_ota_handle_t handle_internal = NULL; + + if (!ota_handle) { + /* Since ota_handle is not present, this default callback might be being called directly externally + * We'll set the bare minimum fields required by the esp_rmaker_report_status. + */ + handle_internal = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_ota_t)); + if(handle_internal == NULL){ + ESP_LOGE(TAG, "Failed to allocate memory for OTA handle."); + return ESP_ERR_NO_MEM; + } + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *) handle_internal; + ota->report_fn = ota_data->report_fn; + ota->transient_priv = (void *) ota_data->ota_job_id; + ota_handle = handle_internal; + } + /* Handle OTA metadata, if any */ if (ota_data->metadata) { if (esp_rmaker_ota_handle_metadata(ota_handle, ota_data) != OTA_OK) { @@ -386,7 +410,9 @@ esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmak if (err != ESP_OK) { ESP_LOGE(TAG, "ESP HTTPS OTA Begin failed"); esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "ESP HTTPS OTA Begin failed"); - return ESP_FAIL; + + err = ESP_FAIL; + goto end; } #ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI @@ -488,7 +514,7 @@ esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmak ESP_LOGI(TAG, "OTA upgrade successful. Auto reboot is disabled. Requesting a Reboot via Event handler."); esp_rmaker_ota_post_event(RMAKER_OTA_EVENT_REQ_FOR_REBOOT, NULL, 0); #endif - return ESP_OK; + err = ESP_OK; } else { if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { ESP_LOGE(TAG, "Image validation failed, image is corrupted"); @@ -500,7 +526,12 @@ esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmak ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed %d", ota_finish_err); } } - return ESP_FAIL; + +end: + if (handle_internal) { + free(handle_internal); + } + return err; } static void event_handler(void* arg, esp_event_base_t event_base, diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h b/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h index 218cba76..01996fcb 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h @@ -41,6 +41,7 @@ typedef struct { ota_status_t last_reported_status; void *transient_priv; char *metadata; + esp_rmaker_ota_report_fn_t report_fn; } esp_rmaker_ota_t; char *esp_rmaker_ota_status_to_string(ota_status_t status); @@ -48,8 +49,8 @@ void esp_rmaker_ota_common_cb(void *priv); void esp_rmaker_ota_finish_using_params(esp_rmaker_ota_t *ota); void esp_rmaker_ota_finish_using_topics(esp_rmaker_ota_t *ota); esp_err_t esp_rmaker_ota_enable_using_params(esp_rmaker_ota_t *ota); -esp_err_t esp_rmaker_ota_report_status_using_params(esp_rmaker_ota_handle_t ota_handle, +esp_err_t esp_rmaker_ota_report_status_using_params(char *ota_job_id, ota_status_t status, char *additional_info); esp_err_t esp_rmaker_ota_enable_using_topics(esp_rmaker_ota_t *ota); -esp_err_t esp_rmaker_ota_report_status_using_topics(esp_rmaker_ota_handle_t ota_handle, +esp_err_t esp_rmaker_ota_report_status_using_topics(char *ota_job_id, ota_status_t status, char *additional_info); diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c index 3c2cece6..0cbf8a83 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_params.c @@ -82,7 +82,7 @@ static esp_err_t esp_rmaker_ota_service_cb(const esp_rmaker_device_t *device, co return ESP_FAIL; } -esp_err_t esp_rmaker_ota_report_status_using_params(esp_rmaker_ota_handle_t ota_handle, ota_status_t status, char *additional_info) +esp_err_t esp_rmaker_ota_report_status_using_params(char *ota_job_id, ota_status_t status, char *additional_info) { const esp_rmaker_device_t *device = esp_rmaker_node_get_device_by_name(esp_rmaker_get_node(), ESP_RMAKER_OTA_SERV_NAME); if (!device) { @@ -100,6 +100,7 @@ esp_err_t esp_rmaker_ota_report_status_using_params(esp_rmaker_ota_handle_t ota_ /* Enable the ESP RainMaker specific OTA */ esp_err_t esp_rmaker_ota_enable_using_params(esp_rmaker_ota_t *ota) { + ota->report_fn = esp_rmaker_ota_report_status_using_params; esp_rmaker_device_t *service = esp_rmaker_ota_service_create(ESP_RMAKER_OTA_SERV_NAME, ota); if (!service) { ESP_LOGE(TAG, "Failed to create OTA Service"); diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c index 1a3887ec..0db43a3c 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_using_topics.c @@ -41,19 +41,19 @@ static uint64_t ota_autofetch_period = (OTA_AUTOFETCH_PERIOD * 60 * 60 * 1000000 static const char *TAG = "esp_rmaker_ota_using_topics"; -esp_err_t esp_rmaker_ota_report_status_using_topics(esp_rmaker_ota_handle_t ota_handle, ota_status_t status, char *additional_info) +esp_err_t esp_rmaker_ota_report_status_using_topics(char *ota_job_id, ota_status_t status, char *additional_info) { - if (!ota_handle) { - return ESP_FAIL; + if (!ota_job_id) { + ESP_LOGE(TAG, "Reporting failed. OTA Job ID not found"); + return ESP_ERR_INVALID_ARG; } - esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; char publish_payload[200]; json_gen_str_t jstr; json_gen_str_start(&jstr, publish_payload, sizeof(publish_payload), NULL, NULL); json_gen_start_object(&jstr); - if (ota->transient_priv) { - json_gen_obj_set_string(&jstr, "ota_job_id", (char *)ota->transient_priv); + if (ota_job_id) { + json_gen_obj_set_string(&jstr, "ota_job_id", ota_job_id); } else { /* This will get executed only when the OTA status is being reported after a reboot, either to * indicate successful verification of new firmware, or to indicate that firmware was rolled back @@ -321,6 +321,7 @@ static void esp_rmaker_ota_work_fn(void *priv_data) /* Enable the ESP RainMaker specific OTA */ esp_err_t esp_rmaker_ota_enable_using_topics(esp_rmaker_ota_t *ota) { + ota->report_fn = esp_rmaker_ota_report_status_using_topics; esp_err_t err = esp_rmaker_work_queue_add_task(esp_rmaker_ota_work_fn, ota); if (err == ESP_OK) { ESP_LOGI(TAG, "OTA enabled with Topics"); From 60bb022227bec4ca48d81cee8985d63ab37cb836 Mon Sep 17 00:00:00 2001 From: Shreyash Bubane Date: Fri, 23 Aug 2024 18:24:40 +0530 Subject: [PATCH 2/4] expose functions from esp_rmaker_ota.c - for checking if OTA validation is pending - for erasing OTA rollback flag from NVS --- .../esp_rainmaker/include/esp_rmaker_ota.h | 22 +++++++++++++++++++ .../esp_rainmaker/src/ota/esp_rmaker_ota.c | 20 +++++++++++------ .../src/ota/esp_rmaker_ota_internal.h | 1 - 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/components/esp_rainmaker/include/esp_rmaker_ota.h b/components/esp_rainmaker/include/esp_rmaker_ota.h index 5c5e2413..e574bf4f 100644 --- a/components/esp_rainmaker/include/esp_rmaker_ota.h +++ b/components/esp_rainmaker/include/esp_rmaker_ota.h @@ -55,6 +55,13 @@ typedef enum { OTA_STATUS_REJECTED, } ota_status_t; +/** Returns string representation of ota_status_t for reporting + * + * @param status ota_status_t variant + * @return string representation for provided status, "invalid" if not valid status + */ +char *esp_rmaker_ota_status_to_string(ota_status_t status); + /** OTA Workflow type */ typedef enum { /** OTA will be performed using services and parameters. */ @@ -238,6 +245,21 @@ esp_err_t esp_rmaker_ota_report_status(esp_rmaker_ota_handle_t ota_handle, ota_s * */ esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t handle, esp_rmaker_ota_data_t *ota_data); +/** Clear Rollback flag + * The default OTA callback stores a value in NVS as a flag to denote if an OTA was recently installed. + * This flag is then read to decide if firmware has been rolled back. + * This function can be called to erase that flag. + */ +esp_err_t esp_rmaker_ota_erase_rollback_flag(void); + +/** Returns whether OTA validation is pending. + * Returns true if firmware validation is pending after an OTA. + * This can be reset using esp_rmaker_ota_erase_rollback_flag() + * + * @return true if validation is pending, false otherwises + */ +bool esp_rmaker_ota_is_ota_validation_pending(void); + /** Fetch OTA Info * * For OTA using Topics, this API can be used to explicitly ask the backend if an OTA is available. diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c index bf570a24..db14614f 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota.c +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c @@ -623,7 +623,7 @@ static esp_err_t esp_ota_check_for_mqtt(esp_rmaker_ota_t *ota) return esp_event_handler_register(RMAKER_COMMON_EVENT, RMAKER_MQTT_EVENT_CONNECTED, &event_handler, ota); } -static esp_err_t esp_rmaker_erase_rollback_flag(void) +esp_err_t esp_rmaker_ota_erase_rollback_flag(void) { nvs_handle handle; esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); @@ -635,20 +635,26 @@ static esp_err_t esp_rmaker_erase_rollback_flag(void) return ESP_OK; } -static void esp_rmaker_ota_manage_rollback(esp_rmaker_ota_t *ota) -{ - /* If rollback is enabled, and the ota update flag is found, it means that the OTA validation is pending - */ +bool esp_rmaker_ota_is_ota_validation_pending(void) { nvs_handle handle; esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); if (err == ESP_OK) { uint8_t ota_update = 0; size_t len = sizeof(ota_update); + + /* If rollback is enabled, and the ota update flag is found, it means that the OTA validation is pending */ if ((err = nvs_get_blob(handle, RMAKER_OTA_UPDATE_FLAG_NVS_NAME, &ota_update, &len)) == ESP_OK) { - ota->validation_in_progress = true; + return true; } nvs_close(handle); } + + return false; +} + +static void esp_rmaker_ota_manage_rollback(esp_rmaker_ota_t *ota) +{ + ota->validation_in_progress = esp_rmaker_ota_is_ota_validation_pending(); 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) { @@ -685,7 +691,7 @@ static void esp_rmaker_ota_manage_rollback(esp_rmaker_ota_t *ota) */ if (ota->validation_in_progress) { ota->rolled_back = true; - esp_rmaker_erase_rollback_flag(); + esp_rmaker_ota_erase_rollback_flag(); if (ota->type == OTA_USING_PARAMS) { /* Calling this only for OTA_USING_PARAMS, because for OTA_USING_TOPICS, * the work queue function will manage the status reporting later. diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h b/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h index 01996fcb..5e0fe654 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h @@ -44,7 +44,6 @@ typedef struct { esp_rmaker_ota_report_fn_t report_fn; } esp_rmaker_ota_t; -char *esp_rmaker_ota_status_to_string(ota_status_t status); void esp_rmaker_ota_common_cb(void *priv); void esp_rmaker_ota_finish_using_params(esp_rmaker_ota_t *ota); void esp_rmaker_ota_finish_using_topics(esp_rmaker_ota_t *ota); From aff970106f26cb202235c64dd6a2214aaa79edcb Mon Sep 17 00:00:00 2001 From: Shreyash Bubane Date: Mon, 26 Aug 2024 11:37:02 +0530 Subject: [PATCH 3/4] here comes OTA Over HTTPS --- components/rmaker_ota_https/CMakeLists.txt | 7 + components/rmaker_ota_https/Kconfig.projbuild | 26 + components/rmaker_ota_https/LICENSE | 201 ++++++ components/rmaker_ota_https/README.md | 16 + components/rmaker_ota_https/idf_component.yml | 15 + .../include/esp_rmaker_ota_https.h | 28 + .../src/esp_rmaker_client_data.c | 177 +++++ .../src/esp_rmaker_client_data.h | 31 + .../src/esp_rmaker_ota_https.c | 680 ++++++++++++++++++ .../src/esp_rmaker_ota_https_internal.h | 38 + .../src/rmaker_node_api_constants.h | 33 + 11 files changed, 1252 insertions(+) create mode 100644 components/rmaker_ota_https/CMakeLists.txt create mode 100644 components/rmaker_ota_https/Kconfig.projbuild create mode 100644 components/rmaker_ota_https/LICENSE create mode 100644 components/rmaker_ota_https/README.md create mode 100644 components/rmaker_ota_https/idf_component.yml create mode 100644 components/rmaker_ota_https/include/esp_rmaker_ota_https.h create mode 100644 components/rmaker_ota_https/src/esp_rmaker_client_data.c create mode 100644 components/rmaker_ota_https/src/esp_rmaker_client_data.h create mode 100644 components/rmaker_ota_https/src/esp_rmaker_ota_https.c create mode 100644 components/rmaker_ota_https/src/esp_rmaker_ota_https_internal.h create mode 100644 components/rmaker_ota_https/src/rmaker_node_api_constants.h diff --git a/components/rmaker_ota_https/CMakeLists.txt b/components/rmaker_ota_https/CMakeLists.txt new file mode 100644 index 00000000..fb246de0 --- /dev/null +++ b/components/rmaker_ota_https/CMakeLists.txt @@ -0,0 +1,7 @@ +set(priv_req rmaker_common esp_rainmaker esp_timer esp_netif esp_http_client esp-tls app_update mbedtls esp_schedule json_parser json_generator) + +idf_component_register(SRC_DIRS "src" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "src" + PRIV_REQUIRES ${priv_req} + ) diff --git a/components/rmaker_ota_https/Kconfig.projbuild b/components/rmaker_ota_https/Kconfig.projbuild new file mode 100644 index 00000000..03a2712b --- /dev/null +++ b/components/rmaker_ota_https/Kconfig.projbuild @@ -0,0 +1,26 @@ +menu "ESP RainMaker HTTPS OTA Config" + config OTA_HTTPS_AUTOFETCH_ENABLED + bool "Enable Autofetch" + default y + help + Enable periodic checking of OTA. + + config OTA_HTTPS_AUTOFETCH_PERIOD + int "Autofetch Period" + depends on OTA_HTTPS_AUTOFETCH_ENABLED + default 2 + help + The time period(in hours) for checking OTA. + If disabled, OTA is only checked once, when HTTPS OTA is enabled. + + config OTA_HTTPS_ROLLBACK_PERIOD + int "Rollback Period" + default 90 + help + The time period(in seconds) for checking if OTA is valid. + If OTA is not marked valid in this timeframe, firmware is rolled back. + + config OTA_HTTPS_APPLY_STACK_SIZE + int "Stack size(in kilobytes) of the task reponsible for calling the OTA callback" + default 3072 +endmenu \ No newline at end of file diff --git a/components/rmaker_ota_https/LICENSE b/components/rmaker_ota_https/LICENSE new file mode 100644 index 00000000..d613ddc8 --- /dev/null +++ b/components/rmaker_ota_https/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Piyush Shah + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/rmaker_ota_https/README.md b/components/rmaker_ota_https/README.md new file mode 100644 index 00000000..60ad42e0 --- /dev/null +++ b/components/rmaker_ota_https/README.md @@ -0,0 +1,16 @@ +# ESP RainMaker HTTPS OTA + +This contains the ESP RainMaker component for performing OTA over HTTPS. It uses [RainMaker Node APIs](https://swaggerapis.rainmaker.espressif.com/?urls.primaryName=RainMaker%20Node%20APIs) for retriving the OTA information and uses the callback from RainMaker itself for applying OTA. + +## Rollback Mechanism +Since regular OTA rollback is based on MQTT connection(which isn't applicable in this case), rollback mechanism works as follows: +1. A rollback timer is created for specified period. +2. Devices tries to report "successful" for firmware. +3. If reporting is successful, firmware is marked valid and rollback timer is canceled. +4. If reporting fails, it is retried continuosly at 5 seconds interval; until the rollback timer expires. +5. If firmware cannot be verified within the specified rollback timeout, firmware is marked invalid and device reboots in the previous firmware. + +--- + +**Note:** Since this component uses callbacks from generic RainMaker OTA component, callback specific settings(like project name, version verification) needs to be configured from *ESP RainMaker Config -> ESP RainMaker OTA Config* in ESP-IDF menuconfig. +**Note:** It is does not currently work with ESP Secure Cert. \ No newline at end of file diff --git a/components/rmaker_ota_https/idf_component.yml b/components/rmaker_ota_https/idf_component.yml new file mode 100644 index 00000000..61e2807d --- /dev/null +++ b/components/rmaker_ota_https/idf_component.yml @@ -0,0 +1,15 @@ +version: "0.0.1" +description: HTTPS OTA for ESP RainMaker firmware agent +dependencies: + espressif/rmaker_common: + version: "~1.4.6" + override_path: '../rmaker_common/' + espressif/json_parser: + version: "~1.0.3" + override_path: '../json_parser' + espressif/json_generator: + version: "~1.1.1" + override_path: '../json_generator' + espressif/esp_schedule: + version: "~1.2.0" + override_path: '../esp_schedule/' \ No newline at end of file diff --git a/components/rmaker_ota_https/include/esp_rmaker_ota_https.h b/components/rmaker_ota_https/include/esp_rmaker_ota_https.h new file mode 100644 index 00000000..1478f87a --- /dev/null +++ b/components/rmaker_ota_https/include/esp_rmaker_ota_https.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +/** Enables and initializes RainMaker OTA using HTTP transport. + * + * @return ESP_OK if success, failure otherwise +*/ +esp_err_t esp_rmaker_ota_https_enable(esp_rmaker_ota_config_t *ota_config); + +/** Checks if there is any ota available and applies the OTA if found + * + * @return ESP_OK if success, failure otherwise + */ +esp_err_t esp_rmaker_ota_https_fetch(void); + +/** Marks the current OTA partition as valid + * + * @return ESP_OK if success, failure otherwise + */ +esp_err_t esp_rmaker_ota_https_mark_valid(void); + +/** Marks the current OTA partition as invalid as reboots + * + * @return ESP_OK if success, failure otherwise + */ +esp_err_t esp_rmaker_ota_https_mark_invalid(void); \ No newline at end of file diff --git a/components/rmaker_ota_https/src/esp_rmaker_client_data.c b/components/rmaker_ota_https/src/esp_rmaker_client_data.c new file mode 100644 index 00000000..21dd0cfd --- /dev/null +++ b/components/rmaker_ota_https/src/esp_rmaker_client_data.c @@ -0,0 +1,177 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include + +#include +#include +#include + +#include "esp_rmaker_client_data.h" + +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + #include "esp_secure_cert_read.h" + /* + * Since TAG is not used in any other place in this file at the moment, + * it has been placed inside this #ifdef to avoid -Werror=unused-variable. + */ + static const char *TAG = "esp_rmaker_client_data"; +#endif + +extern uint8_t mqtt_server_root_ca_pem_start[] asm("_binary_rmaker_mqtt_server_crt_start"); +extern uint8_t mqtt_server_root_ca_pem_end[] asm("_binary_rmaker_mqtt_server_crt_end"); + +char * esp_rmaker_get_mqtt_host() +{ +#ifdef CONFIG_ESP_RMAKER_READ_MQTT_HOST_FROM_CONFIG + return strdup(CONFIG_ESP_RMAKER_MQTT_HOST); +#else + char *host = esp_rmaker_factory_get(ESP_RMAKER_MQTT_HOST_NVS_KEY); +#if defined(CONFIG_ESP_RMAKER_SELF_CLAIM) || defined(CONFIG_ESP_RMAKER_ASSISTED_CLAIM) + if (!host) { + return strdup(CONFIG_ESP_RMAKER_MQTT_HOST); + } +#endif /* defined(CONFIG_ESP_RMAKER_SELF_CLAIM) || defined(CONFIG_ESP_RMAKER_ASSISTED_CLAIM) */ + return host; +#endif /* !CONFIG_ESP_RMAKER_READ_MQTT_HOST_FROM_CONFIG */ +} + +char * esp_rmaker_get_client_cert() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_cert_len = 0; + char *client_cert_addr = NULL; + if (esp_secure_cert_get_device_cert(&client_cert_addr, &client_cert_len) == ESP_OK) { + return client_cert_addr; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of device cert"); + ESP_LOGI(TAG, "Attempting to fetch client certificate from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get(ESP_RMAKER_CLIENT_CERT_NVS_KEY); +} + +size_t esp_rmaker_get_client_cert_len() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_cert_len = 0; + char *client_cert_addr = NULL; + if (esp_secure_cert_get_device_cert(&client_cert_addr, &client_cert_len) == ESP_OK) { + return client_cert_len; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of device cert"); + ESP_LOGI(TAG, "Attempting to fetch client certificate from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get_size(ESP_RMAKER_CLIENT_CERT_NVS_KEY) + 1; /* +1 for NULL terminating byte */ +} + +char * esp_rmaker_get_client_key() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_key_len = 0; + char *client_key_addr = NULL; + if (esp_secure_cert_get_priv_key(&client_key_addr, &client_key_len) == ESP_OK) { + return client_key_addr; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of private_key"); + ESP_LOGI(TAG, "Attempting to fetch key from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get(ESP_RMAKER_CLIENT_KEY_NVS_KEY); +} + +size_t esp_rmaker_get_client_key_len() +{ +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + uint32_t client_key_len = 0; + char *client_key_addr = NULL; + if (esp_secure_cert_get_priv_key(&client_key_addr, &client_key_len) == ESP_OK) { + return client_key_len; + } else { + ESP_LOGE(TAG, "Failed to obtain flash address of private_key"); + ESP_LOGI(TAG, "Attempting to fetch key from NVS"); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + return esp_rmaker_factory_get_size(ESP_RMAKER_CLIENT_KEY_NVS_KEY) + 1; /* +1 for NULL terminating byte */ +} + +char * esp_rmaker_get_client_csr() +{ + return esp_rmaker_factory_get(ESP_RMAKER_CLIENT_CSR_NVS_KEY); +} + +esp_rmaker_mqtt_conn_params_t *esp_rmaker_get_mqtt_conn_params() +{ + esp_rmaker_mqtt_conn_params_t *mqtt_conn_params = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_mqtt_conn_params_t)); + +#if defined(CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR) && defined(CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL) + mqtt_conn_params->ds_data = esp_secure_cert_get_ds_ctx(); + if (mqtt_conn_params->ds_data == NULL) /* Get client key only if ds_data is NULL */ +#endif /* (defined(CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR) && defined(CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL)) */ + { + if ((mqtt_conn_params->client_key = esp_rmaker_get_client_key()) == NULL) { + goto init_err; + } + mqtt_conn_params->client_key_len = esp_rmaker_get_client_key_len(); + } + if ((mqtt_conn_params->client_cert = esp_rmaker_get_client_cert()) == NULL) { + goto init_err; + } + mqtt_conn_params->client_cert_len = esp_rmaker_get_client_cert_len(); + if ((mqtt_conn_params->mqtt_host = esp_rmaker_get_mqtt_host()) == NULL) { + goto init_err; + } + mqtt_conn_params->server_cert = (char *)mqtt_server_root_ca_pem_start; + mqtt_conn_params->client_id = esp_rmaker_get_node_id(); + return mqtt_conn_params; +init_err: + esp_rmaker_clean_mqtt_conn_params(mqtt_conn_params); + free(mqtt_conn_params); + return NULL; +} + +void esp_rmaker_clean_mqtt_conn_params(esp_rmaker_mqtt_conn_params_t *mqtt_conn_params) +{ + if (mqtt_conn_params) { + if (mqtt_conn_params->mqtt_host) { + free(mqtt_conn_params->mqtt_host); + } +#ifdef CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR + if (mqtt_conn_params->client_cert) { + esp_secure_cert_free_device_cert(mqtt_conn_params->client_cert); + } +#ifdef CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL + if (mqtt_conn_params->ds_data) { + esp_secure_cert_free_ds_ctx(mqtt_conn_params->ds_data); + } +#else /* !CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL */ + if (mqtt_conn_params->client_key) { + esp_secure_cert_free_priv_key(mqtt_conn_params->client_key); + } +#endif /* CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL */ +#else /* !CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + if (mqtt_conn_params->client_cert) { + free(mqtt_conn_params->client_cert); + } + if (mqtt_conn_params->client_key) { + free(mqtt_conn_params->client_key); + } +#endif /* CONFIG_ESP_RMAKER_USE_ESP_SECURE_CERT_MGR */ + } +} diff --git a/components/rmaker_ota_https/src/esp_rmaker_client_data.h b/components/rmaker_ota_https/src/esp_rmaker_client_data.h new file mode 100644 index 00000000..f89d5733 --- /dev/null +++ b/components/rmaker_ota_https/src/esp_rmaker_client_data.h @@ -0,0 +1,31 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include + +#define ESP_RMAKER_CLIENT_CERT_NVS_KEY "client_cert" +#define ESP_RMAKER_CLIENT_KEY_NVS_KEY "client_key" +#define ESP_RMAKER_MQTT_HOST_NVS_KEY "mqtt_host" +#define ESP_RMAKER_CLIENT_CSR_NVS_KEY "csr" +#define ESP_RMAKER_CLIENT_RANDOM_NVS_KEY "random" + +char *esp_rmaker_get_client_cert(); +size_t esp_rmaker_get_client_cert_len(); +char *esp_rmaker_get_client_key(); +size_t esp_rmaker_get_client_key_len(); +char *esp_rmaker_get_client_csr(); +char *esp_rmaker_get_mqtt_host(); +esp_rmaker_mqtt_conn_params_t *esp_rmaker_get_mqtt_conn_params(); +void esp_rmaker_clean_mqtt_conn_params(esp_rmaker_mqtt_conn_params_t *mqtt_conn_params); diff --git a/components/rmaker_ota_https/src/esp_rmaker_ota_https.c b/components/rmaker_ota_https/src/esp_rmaker_ota_https.c new file mode 100644 index 00000000..b38761dc --- /dev/null +++ b/components/rmaker_ota_https/src/esp_rmaker_ota_https.c @@ -0,0 +1,680 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "esp_rmaker_client_data.h" +#include "esp_rmaker_ota_https.h" +#include "esp_rmaker_ota_https_internal.h" +#include "rmaker_node_api_constants.h" + +#ifdef CONFIG_OTA_HTTPS_AUTOFETCH_ENABLED + #include + /* Convert hours to microseconds */ + #define OTA_AUTOFETCH_PERIOD CONFIG_OTA_HTTPS_AUTOFETCH_PERIOD * 3600 * 1000000 +#endif + +static const char *TAG = "esp_rmaker_ota_https"; +static esp_rmaker_ota_https_t *g_ota_https_data; +static EventGroupHandle_t event_group_apply_update; +#define OTA_APPLIED_BIT BIT0 + +static const esp_rmaker_ota_config_t ota_default_config = { + .ota_cb = esp_rmaker_ota_default_cb +}; + +static esp_err_t ota_https_nvs_erase_job_id(void){ + nvs_handle nvs; + if(nvs_open_from_partition(OTA_HTTPS_NVS_PART_NAME, OTA_HTTPS_NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK){ + if(nvs_erase_key(nvs, OTA_HTTPS_JOB_ID_NVS_NAME) != ESP_OK){ + return ESP_OK; + } + } + + return ESP_FAIL; +} + +esp_err_t esp_rmaker_ota_https_report(char *ota_job_id, ota_status_t status, char *additional_info) +{ + + /* Actual url will be 59 bytes. */ + char ota_report_url[64]; + snprintf(ota_report_url, 64, "%s/%s", NODE_API_ENDPOINT_BASE, NODE_API_ENDPOINT_SUFFIX_REPORT); + + char publish_payload[200]; + json_gen_str_t jstr; + json_gen_str_start(&jstr, publish_payload, sizeof(publish_payload), NULL, NULL); + json_gen_start_object(&jstr); + + if(ota_job_id){ + json_gen_obj_set_string(&jstr, "ota_job_id", ota_job_id); + } else { + /* Reporting after applying and reboot, we need to fetch ota_job_id from NVS */ + nvs_handle nvs; + if(nvs_open_from_partition(OTA_HTTPS_NVS_PART_NAME, OTA_HTTPS_NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK){ + char job_id[64] = {0}; + size_t job_id_len = sizeof(job_id); + if(nvs_get_blob( + nvs, OTA_HTTPS_JOB_ID_NVS_NAME, &job_id, &job_id_len) == ESP_OK){ + json_gen_obj_set_string(&jstr, "ota_job_id", job_id); + } else { + ESP_LOGE(TAG, "Failed to report: ota_job_id not found in NVS"); + return ESP_FAIL; + } + } else { + ESP_LOGE(TAG, "Failed to open NVS for reading ota_job_id"); + return ESP_FAIL; + } + } + + json_gen_obj_set_string(&jstr, "status", esp_rmaker_ota_status_to_string(status)); + json_gen_obj_set_string(&jstr, "additional_info", additional_info); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + + const char *client_cert = esp_rmaker_get_client_cert(); + const char *client_key = esp_rmaker_get_client_key(); + + if (!client_cert || !client_key) { + if (client_cert) { + free((char *)client_cert); + } + if (client_key) { + free((char *)client_key); + } + return ESP_ERR_INVALID_STATE; + } + + esp_http_client_config_t http_config = { + .url = ota_report_url, + .client_cert_pem = client_cert, + .client_key_pem = client_key, + .crt_bundle_attach = esp_crt_bundle_attach, + .method = HTTP_METHOD_POST + }; + + esp_http_client_handle_t client = esp_http_client_init(&http_config); + if(!client){ + ESP_LOGE(TAG, "Failed to initialize HTTP client."); + free((char *)client_cert); + free((char *)client_key); + return ESP_FAIL; + } + + esp_http_client_set_header(client, "Content-Type", "application/json"); + + esp_http_client_set_post_field(client, publish_payload, strlen(publish_payload)); + esp_err_t err = esp_http_client_perform(client); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to perform HTTP request."); + err = ESP_FAIL; + goto end; + } + + int response_status_code = esp_http_client_get_status_code(client); + if (response_status_code != 200) + { + ESP_LOGE(TAG, "Failed to report status: %d", response_status_code); + ESP_LOGE(TAG, "writing to server: %s, payload: %s", ota_report_url, publish_payload); + err = ESP_FAIL; + } + +end: + esp_http_client_cleanup(client); + free((char *)client_cert); + free((char *)client_key); + return err; +} + +static void esp_rmaker_ota_https_finish(esp_rmaker_ota_https_t *ota){ + if (!ota) { + return; + } + + if (ota->ota_url){ + free(ota->ota_url); + ota->ota_url = NULL; + } + + if (ota->fw_version){ + free(ota->fw_version); + ota->fw_version = NULL; + } + + if (ota->ota_job_id){ + free(ota->ota_job_id); + ota->ota_job_id = NULL; + } + + if(ota->metadata){ + free(ota->metadata); + ota->metadata = NULL; + } + + if (ota->filesize){ + ota->filesize = 0; + } + + ota->ota_in_progress = false; +} + + +static void _apply_ota_task(void *arg){ + esp_rmaker_ota_https_t *ota = (esp_rmaker_ota_https_t *)arg; + + if(!ota->ota_cb){ + ESP_LOGE(TAG, "OTA Callback not registered"); + goto end; + } + + esp_rmaker_ota_data_t ota_data; + ota_data.fw_version = ota->fw_version; + ota_data.filesize = ota->filesize; + ota_data.metadata = ota->metadata; + ota_data.ota_job_id = ota->ota_job_id; + ota_data.priv = ota->priv; + ota_data.report_fn = esp_rmaker_ota_https_report; + ota_data.url = ota->ota_url; + + if (ota->ota_cb(NULL, &ota_data) != ESP_OK){ + ESP_LOGE(TAG, "Failed to apply OTA"); + } + +end: + xEventGroupSetBits(event_group_apply_update, OTA_APPLIED_BIT); + vTaskDelete(NULL); +} + +static esp_err_t apply_ota(esp_rmaker_ota_https_t *ota) +{ + event_group_apply_update = xEventGroupCreate(); + if (!event_group_apply_update){ + ESP_LOGE(TAG, "Unable to create event group for applying OTA"); + return ESP_ERR_NO_MEM; + } + + if(xTaskCreate(_apply_ota_task, "apply_ota", CONFIG_OTA_HTTPS_APPLY_STACK_SIZE, ota, 5, NULL) != pdPASS){ + ESP_LOGE(TAG, "Failed to create task for applying OTA"); + vEventGroupDelete(event_group_apply_update); + return ESP_ERR_NO_MEM; + } + + xEventGroupWaitBits(event_group_apply_update, OTA_APPLIED_BIT, pdFALSE, pdTRUE, portMAX_DELAY); + vEventGroupDelete(event_group_apply_update); + return ESP_OK; +} + +static esp_err_t handle_fetched_data(char* json_payload) +{ + esp_err_t err = ESP_OK; + if(!json_payload){ + return ESP_ERR_INVALID_ARG; + } + + jparse_ctx_t jctx; + + int ret = json_parse_start(&jctx, json_payload, strlen(json_payload)); + if (ret != OS_SUCCESS){ + ESP_LOGE(TAG, "Failed to start json parser."); + return ESP_FAIL; + }; + + /* RainMaker node API returns 404 with a specific error code when there is no OTA available. + * We first check if `error_code` is present in the response. + * If we find it, we can further check if that denotes no pending OTA or some other error + * Otherwise, the reponse contains actual OTA info and we can parse it accordingly */ + + int error_code = -1; + ret = json_obj_get_int(&jctx, NODE_API_FIELD_ERROR_CODE, &error_code); + if(error_code == -1){ + /* OTA update found */ + char* buff; + int len; + esp_rmaker_ota_https_t *ota = g_ota_https_data; + ota->ota_in_progress = true; + + /* Extract JOB_ID from payload */ + ret = json_obj_get_strlen(&jctx, NODE_API_FIELD_JOB_ID, &len); + if (ret) { + ESP_LOGE(TAG, "ota_job_id not found in OTA update."); + err = ESP_ERR_INVALID_ARG; + goto end; + } + + buff = MEM_ALLOC_EXTRAM(len+1); + if(!buff){ + ESP_LOGE(TAG, "Could not allocate %d bytes for OTA JOBID", len); + err = ESP_ERR_NO_MEM; + goto end; + } + json_obj_get_string(&jctx, NODE_API_FIELD_JOB_ID, buff, len+1); + ota->ota_job_id = buff; + buff = NULL; + + nvs_handle nvs; + if(nvs_open_from_partition(OTA_HTTPS_NVS_PART_NAME, OTA_HTTPS_NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK){ + nvs_set_blob(nvs, OTA_HTTPS_JOB_ID_NVS_NAME, ota->ota_job_id, len); + nvs_close(nvs); + } + + /* Extract URL from payload */ + ret = json_obj_get_strlen(&jctx, NODE_API_FIELD_URL, &len); + if (ret) { + ESP_LOGE(TAG, "Url not found in OTA update."); + esp_rmaker_ota_https_report(ota->ota_job_id, OTA_STATUS_REJECTED, "URL not present"); + err = ESP_ERR_INVALID_ARG; + goto end; + } + + buff = MEM_ALLOC_EXTRAM(len+1); + if(!buff){ + ESP_LOGE(TAG, "Could not allocate %d bytes for OTA URL", len); + esp_rmaker_ota_https_report(ota->ota_job_id, OTA_STATUS_REJECTED, "Failed to allocate memory for URL"); + err = ESP_ERR_NO_MEM; + goto end; + } + json_obj_get_string(&jctx, NODE_API_FIELD_URL, buff, len+1); + ota->ota_url = buff; + buff = NULL; + + /* Try to extract metadata from payload + * Metadata is sent in the response only if it is applicable for a job. + * Hence OTA will not be rejected if metadata key is not found in JSON. + */ + + /* Responding to time information requires rainmaker time server and a otafetch callback. + * Disabling metadata parsing until it is implemented. + */ + + #ifdef ESP_RMAKER_OTA_HTTPS_EXTRACT_METADATA + ret = json_obj_get_object_strlen(&jctx, NODE_API_FIELD_METADATA, &len); + if (!ret) { + ESP_LOGI(TAG, "Metadata present for OTA Job "); + buff = MEM_ALLOC_EXTRAM(len+1); + if(!buff){ + ESP_LOGE(TAG, "Could not allocate %d bytes for OTA metadata", len); + esp_rmaker_ota_https_report(ota->ota_job_id, OTA_STATUS_REJECTED, "Failed to allocate memory for metadata"); + err = ESP_ERR_NO_MEM; + goto end; + } + + json_obj_get_object_str(&jctx, NODE_API_FIELD_METADATA, buff, len+1); + ota->metadata = buff; + buff = NULL; + } + /* Extract Firmware Version from payload */ + ret = json_obj_get_strlen(&jctx, NODE_API_FIELD_FW_VERSION, &len); + buff = MEM_ALLOC_EXTRAM(len+1); + if(buff){ + json_obj_get_string(&jctx, NODE_API_FIELD_FW_VERSION, buff, len+1); + ota->fw_version = buff; + buff = NULL; + } + #endif + + + /* Extract File Size from payload */ + int filesize; + json_obj_get_int(&jctx, NODE_API_FIELD_FILE_SIZE, &filesize); + ota->filesize = filesize; + + ESP_LOGD(TAG, "Parsed OTA Info-\nurl=%s\nota_job_id=%s\nfw_version=%s\nfilesize=%d", ota->ota_url, ota->ota_job_id, ota->fw_version, ota->filesize); + if (!ota->ota_cb){ + ESP_LOGE(TAG, "Failed to process OTA: callback not found"); + err = ESP_ERR_INVALID_ARG; + goto end; + } + + /* The default callback from esp_rmaker_ota.c does not close esp_https_ota client + * before reporting rejected(due to for e.g., invalid project version/name). + * Hence, creating a separate task for this purpose. + */ + apply_ota(ota); + + } else { + /* Error */ + if(error_code == NODE_API_ERROR_CODE_NO_UPDATE_AVAILABLE){ + ESP_LOGI(TAG, "No OTA update available"); + goto end; + } + + char* description_buff; + int description_len; + + json_obj_get_strlen(&jctx, NODE_API_FIELD_DESCRIPTION, &description_len); + description_buff = malloc(description_len+1); /* Also accounting NULL byte */ + + if(description_buff){ + json_obj_get_string(&jctx, NODE_API_FIELD_DESCRIPTION, description_buff, description_len+1); + description_buff[description_len] = '\0'; + ESP_LOGE(TAG, "Error while fetching OTA update - %s", description_buff); + free(description_buff); + } + } + +end: + json_parse_end(&jctx); + esp_rmaker_ota_https_finish(g_ota_https_data); + return err; +} + +esp_err_t esp_rmaker_ota_https_fetch(void) +{ + esp_rmaker_ota_https_t *ota = g_ota_https_data; + + if (!ota){ + return ESP_ERR_INVALID_STATE; + } + + if (ota->ota_in_progress){ + ESP_LOGW(TAG, "Skipping OTA Fetch as an OTA is already in progress"); + return ESP_ERR_INVALID_STATE; + } + + /* Buffer size according to default API endpoint. + * May need more space for other URLS. + */ + char ota_fetch_url[64]; + snprintf(ota_fetch_url, 64, "%s/%s", NODE_API_ENDPOINT_BASE, NODE_API_ENDPOINT_SUFFIX_FETCH); + + const char *client_cert = esp_rmaker_get_client_cert(); + const char *client_key = esp_rmaker_get_client_key(); + + if (!client_cert || !client_key) { + if(client_cert){ + free((char *)client_cert); + } + if (client_key) { + free((char *)client_key); + } + return ESP_ERR_INVALID_STATE; + } + + esp_http_client_config_t http_config = { + .url = ota_fetch_url, + .client_cert_pem = client_cert, + .client_key_pem = client_key, + .crt_bundle_attach = esp_crt_bundle_attach, + .method = HTTP_METHOD_GET + }; + + esp_http_client_handle_t client = esp_http_client_init(&http_config); + esp_err_t err = ESP_OK; + if(!client){ + ESP_LOGE(TAG, "Failed to initialize HTTP client."); + err = ESP_FAIL; + goto ret; + } + + esp_http_client_set_header(client, "Content-Type", "application/json"); + + if ((err = esp_http_client_open(client, 0)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to open HTTP Connection."); + goto end; + } + + int content_len = esp_http_client_fetch_headers(client); + + /* Reserving an extra byte for NULL character. */ + char *buff = MEM_ALLOC_EXTRAM(content_len+1); + if (!buff) { + ESP_LOGE(TAG, "Failed to allocate memory for response buffer."); + err = ESP_ERR_NO_MEM; + goto end; + } + + int read_len = esp_http_client_read(client, buff, content_len); + if (read_len < 0) { + ESP_LOGE(TAG, "Failed to read HTTP Response."); + err = ESP_FAIL; + } else { + buff[read_len] = '\0'; + ESP_LOGI(TAG, "Received OTA: %s", buff); + if (esp_http_client_cleanup(client) != ESP_OK ){ + ESP_LOGW(TAG, "Failed to cleanup HTTP client before handling otafetch response."); + } + err = handle_fetched_data(buff); + if (err != ESP_OK){ + ESP_LOGE(TAG, "Failed to handle received OTA information: %s", esp_err_to_name(err)); + err = ESP_FAIL; + } + goto ret; /* HTTP Client is already cleared */ + } + + free(buff); +end: + esp_http_client_cleanup(client); +ret: + free((char *)client_cert); + free((char *)client_key); + return err; +} + + + +#ifdef CONFIG_OTA_HTTPS_AUTOFETCH_ENABLED +void esp_rmaker_ota_https_autofetch_cb(void *priv_data) +{ + if(esp_rmaker_ota_https_fetch() != ESP_OK){ + ESP_LOGE(TAG, "Failed to fetch OTA update"); + }; +} +static void esp_rmaker_ota_https_register_timer(esp_rmaker_ota_https_t *ota) +{ + if (!ota){ + return; + } + + esp_timer_create_args_t timer_config = { + .name = "ota_https_autofetch", + .callback = esp_rmaker_ota_https_autofetch_cb, + .arg = (void *)ota, + .dispatch_method = ESP_TIMER_TASK, + }; + + if(esp_timer_create(&timer_config, &ota->autofetch_timer) == ESP_OK){ + /* Timer period conversion for silencing overflow warning. + * Theoretically overflow occures if 5124095574 hours provided. + */ + esp_timer_start_periodic(ota->autofetch_timer, (uint64_t) OTA_AUTOFETCH_PERIOD); + } else { + ESP_LOGE(TAG, "Failed to enable autofetch."); + } +} +#endif + +esp_err_t esp_rmaker_ota_https_mark_valid(void) +{ + esp_rmaker_ota_https_t *ota = g_ota_https_data; + if (!ota){ + return ESP_ERR_INVALID_STATE; + } + + ota->ota_valid = true; + + 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) { + ESP_LOGW(TAG, "OTA Already marked as valid"); + return ESP_ERR_INVALID_STATE; + } + } + + if (!ota){ + return ESP_ERR_INVALID_STATE; + } + esp_ota_mark_app_valid_cancel_rollback(); + if(esp_timer_stop(ota->rollback_timer) != ESP_OK){ + ESP_LOGW(TAG, "Failed to stop rollback timemr."); + /* Not returning failure since OTA is already marked as valid and rollback will eventually fail. */ + } + + ota->ota_in_progress = false; + return ESP_OK; +} + +esp_err_t esp_rmaker_ota_https_mark_invalid(void) +{ + return esp_rmaker_ota_mark_invalid(); +} + +static void esp_rmaker_ota_https_rollback(void *args) +{ + if(g_ota_https_data -> ota_valid){ + return; + } + ESP_LOGE(TAG, "Unable to verify firmware. Rolling back."); + esp_rmaker_ota_https_mark_invalid(); +} + +static esp_err_t ota_check_wifi(esp_rmaker_ota_https_t *ota) +{ + esp_timer_create_args_t timer_args = { + .name = "OTA HTTPS Rollback Timer", + .callback = esp_rmaker_ota_https_rollback, + .arg = (void *) ota, + .dispatch_method = ESP_TIMER_TASK, + }; + esp_err_t err = esp_timer_create(&timer_args, &ota->rollback_timer); + if (err != ESP_OK){ + ESP_LOGE(TAG, "Failed to create rollback timer"); + return ESP_FAIL; + } + + esp_timer_start_once(ota->rollback_timer, (CONFIG_OTA_HTTPS_ROLLBACK_PERIOD * 1000 * 1000)); + + /* Verify OTA by trying to send OTA successful report */ + + while(true) { + if (esp_rmaker_ota_https_report(NULL, OTA_STATUS_SUCCESS, "OTA Upgrade finished and verified successfully") == ESP_OK){ + ESP_LOGI(TAG, "Sucessfully reported success for OTA image."); + if(esp_rmaker_ota_https_mark_valid() != ESP_OK){ + ESP_LOGE(TAG, "Failed to mark OTA as valid after reporting success."); + return ESP_FAIL; + } + + if (ota_https_nvs_erase_job_id() != ESP_OK) { + ESP_LOGW(TAG, "Failed to erase OTA ID from NVS."); + } + ESP_LOGI(TAG, "OTA Firmware verification successful"); + return ESP_OK; + } + + /* Wait 5 seconds before retry */ + vTaskDelay(5000/portTICK_PERIOD_MS); + } + + return ESP_OK; +} + +static void esp_rmaker_ota_https_manage_rollback(esp_rmaker_ota_https_t *ota) +{ + if (!ota){ + ESP_LOGE(TAG, "manage_rollback failed due to invalid argument"); + return; + } + + bool validation_pending = esp_rmaker_ota_is_ota_validation_pending(); + 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) { + ESP_LOGI(TAG, "OTA state = %d", ota_state); + /* Not checking for CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE here because the firmware may have + * it disabled, but bootloader may have it enabled, in which case, we will have to + * handle this state. + */ + if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGI(TAG, "First Boot after an OTA"); + /* Run diagnostic function */ + esp_rmaker_ota_diag_status_t diag_status = OTA_DIAG_STATUS_SUCCESS; + if (ota->ota_diag) { + esp_rmaker_ota_diag_priv_t ota_diag_priv = { + .state = OTA_DIAG_STATE_INIT, + .rmaker_ota = validation_pending + }; + diag_status = ota->ota_diag(&ota_diag_priv, ota->priv); + } + if (diag_status != OTA_DIAG_STATUS_FAIL) { + ESP_LOGI(TAG, "Diagnostics completed successfully! Continuing execution ..."); + ota->ota_in_progress = true; + ota_check_wifi(ota); + ota->ota_in_progress = false; + } else { + ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version ..."); + esp_rmaker_ota_https_mark_invalid(); + } + } else { + if (validation_pending) { + esp_rmaker_ota_erase_rollback_flag(); + if(esp_rmaker_ota_https_report(NULL, OTA_STATUS_REJECTED, "Firmware rolled back") != ESP_OK){ + ESP_LOGW(TAG, "Failed to report firmware rolled back."); + } + ota_https_nvs_erase_job_id(); + } + + } + } +} + +esp_err_t esp_rmaker_ota_https_enable(esp_rmaker_ota_config_t *ota_config) +{ + ESP_LOGI(TAG, "Enabling OTA over HTTPS"); + /* Check if OTA is already enabled*/ + static bool ota_https_enabled; + if (ota_https_enabled){ + ESP_LOGI(TAG, "HTTPS OTA already initialised"); + return ESP_ERR_INVALID_SIZE; + } + + esp_rmaker_ota_https_t *ota = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_ota_https_t)); + if (!ota){ + ESP_LOGE(TAG, "Unable to allocate memory for storing OTA information"); + return ESP_ERR_NO_MEM; + } + + if (!ota_config){ + ota_config = (esp_rmaker_ota_config_t *) &ota_default_config; + } + + if (ota_config->ota_cb){ + ota->ota_cb = ota_config->ota_cb; + ota->priv = ota_config->priv; + } else { + ota->ota_cb = &esp_rmaker_ota_default_cb; + ota->priv = NULL; + } + +#ifdef CONFIG_OTA_HTTPS_AUTOFETCH_ENABLED + esp_rmaker_ota_https_register_timer(ota); +#endif + g_ota_https_data = ota; + esp_rmaker_ota_https_manage_rollback(ota); + return ESP_OK; +} \ No newline at end of file diff --git a/components/rmaker_ota_https/src/esp_rmaker_ota_https_internal.h b/components/rmaker_ota_https/src/esp_rmaker_ota_https_internal.h new file mode 100644 index 00000000..77762dc6 --- /dev/null +++ b/components/rmaker_ota_https/src/esp_rmaker_ota_https_internal.h @@ -0,0 +1,38 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +typedef struct { + esp_rmaker_ota_cb_t ota_cb; + esp_rmaker_post_ota_diag_t ota_diag; + void *priv; + bool ota_in_progress; + char *ota_url; + char *fw_version; + char *ota_job_id; + char *metadata; + bool ota_valid; + int filesize; + esp_timer_handle_t autofetch_timer; + esp_timer_handle_t rollback_timer; +} esp_rmaker_ota_https_t; + +/* TODO: find a way to avoid this value duplication */ +#define OTA_HTTPS_NVS_PART_NAME "nvs" +#define OTA_HTTPS_NVS_NAMESPACE "rmaker_ota" +#define OTA_HTTPS_JOB_ID_NVS_NAME "rmaker_ota_id" \ No newline at end of file diff --git a/components/rmaker_ota_https/src/rmaker_node_api_constants.h b/components/rmaker_ota_https/src/rmaker_node_api_constants.h new file mode 100644 index 00000000..263c7464 --- /dev/null +++ b/components/rmaker_ota_https/src/rmaker_node_api_constants.h @@ -0,0 +1,33 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#define NODE_API_ENDPOINT_BASE "https://api.node.rainmaker.espressif.com/v1" + +#define NODE_API_ENDPOINT_SUFFIX_FETCH "node/otafetch" +#define NODE_API_ENDPOINT_SUFFIX_REPORT "node/otastatus" + +/* API returns `105065` error code while the swagger documentation suggests it should return `10561` + * Update the error code here once that is fixed + */ +#define NODE_API_ERROR_CODE_NO_UPDATE_AVAILABLE 105065 + +#define NODE_API_FIELD_ERROR_CODE "error_code" +#define NODE_API_FIELD_DESCRIPTION "description" +#define NODE_API_FIELD_URL "url" +#define NODE_API_FIELD_METADATA "metadata" +#define NODE_API_FIELD_FW_VERSION "fw_version" +#define NODE_API_FIELD_JOB_ID "ota_job_id" +#define NODE_API_FIELD_FILE_SIZE "file_size" \ No newline at end of file From 78a0279e4c5ff47ef623039842cdaf7b51288356 Mon Sep 17 00:00:00 2001 From: Shreyash Bubane Date: Mon, 28 Oct 2024 20:27:39 +0530 Subject: [PATCH 4/4] add an example for HTTPS OTA --- examples/ota_https/CMakeLists.txt | 16 +++ examples/ota_https/Makefile | 12 ++ examples/ota_https/README.md | 12 ++ examples/ota_https/main/CMakeLists.txt | 2 + examples/ota_https/main/Kconfig.projbuild | 11 ++ examples/ota_https/main/app_main.c | 116 +++++++++++++++++ examples/ota_https/partitions.csv | 10 ++ .../ota_https/partitions_4mb_optimised.csv | 11 ++ examples/ota_https/sdkconfig.defaults | 24 ++++ examples/ota_https/sdkconfig.defaults.esp32 | 1 + examples/ota_https/sdkconfig.defaults.esp32c2 | 121 ++++++++++++++++++ examples/ota_https/sdkconfig.defaults.esp32c6 | 10 ++ examples/ota_https/sdkconfig.defaults.esp32h2 | 14 ++ examples/ota_https/sdkconfig.defaults.esp32s2 | 4 + 14 files changed, 364 insertions(+) create mode 100644 examples/ota_https/CMakeLists.txt create mode 100644 examples/ota_https/Makefile create mode 100644 examples/ota_https/README.md create mode 100644 examples/ota_https/main/CMakeLists.txt create mode 100644 examples/ota_https/main/Kconfig.projbuild create mode 100644 examples/ota_https/main/app_main.c create mode 100644 examples/ota_https/partitions.csv create mode 100644 examples/ota_https/partitions_4mb_optimised.csv create mode 100644 examples/ota_https/sdkconfig.defaults create mode 100644 examples/ota_https/sdkconfig.defaults.esp32 create mode 100644 examples/ota_https/sdkconfig.defaults.esp32c2 create mode 100644 examples/ota_https/sdkconfig.defaults.esp32c6 create mode 100644 examples/ota_https/sdkconfig.defaults.esp32h2 create mode 100644 examples/ota_https/sdkconfig.defaults.esp32s2 diff --git a/examples/ota_https/CMakeLists.txt b/examples/ota_https/CMakeLists.txt new file mode 100644 index 00000000..2ab0e17c --- /dev/null +++ b/examples/ota_https/CMakeLists.txt @@ -0,0 +1,16 @@ +# 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) + +if(DEFINED ENV{RMAKER_PATH}) + set(RMAKER_PATH $ENV{RMAKER_PATH}) +else() + set(RMAKER_PATH ${CMAKE_CURRENT_LIST_DIR}/../..) +endif(DEFINED ENV{RMAKER_PATH}) + +# Add RainMaker components and other common application components +set(EXTRA_COMPONENT_DIRS ${RMAKER_PATH}/components ${RMAKER_PATH}/examples/common ${RMAKER_PATH}/components/rmaker_ota_https) + +set(PROJECT_VER "1.0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(ota_https) diff --git a/examples/ota_https/Makefile b/examples/ota_https/Makefile new file mode 100644 index 00000000..b3f7b62c --- /dev/null +++ b/examples/ota_https/Makefile @@ -0,0 +1,12 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := ota_https +PROJECT_VER := 1.0 + +# Add RainMaker components and other common application components +EXTRA_COMPONENT_DIRS += $(PROJECT_PATH)/../../components $(PROJECT_PATH)/../common + +include $(IDF_PATH)/make/project.mk diff --git a/examples/ota_https/README.md b/examples/ota_https/README.md new file mode 100644 index 00000000..94dd4efc --- /dev/null +++ b/examples/ota_https/README.md @@ -0,0 +1,12 @@ +# OTA OVER HTTPS EXAMPLE + +## Build and Flash firmware + +Follow the ESP RainMaker Documentation [Get Started](https://rainmaker.espressif.com/docs/get-started.html) section to build and flash this firmware. Just note the path of this example. + +## What to expect in this example? + +- This example uses ESP RainMaker services for OTA using rmaker_ota_https component. +- It can be used as an reference for using ESP RainMaker as a device management framework in some other use case. +- It has no dependency on the RainMaker phone application. WiFi credentials need to be pre supplied in **"Example Configuration"** in *menuconfig* before running the example/ +- Thereafter, OTA can be supplied from [RainMaker Dashboard](https://dashboard.rainmaker.espressif.com) \ No newline at end of file diff --git a/examples/ota_https/main/CMakeLists.txt b/examples/ota_https/main/CMakeLists.txt new file mode 100644 index 00000000..4d0e0a5f --- /dev/null +++ b/examples/ota_https/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS ./app_main.c + INCLUDE_DIRS ".") \ No newline at end of file diff --git a/examples/ota_https/main/Kconfig.projbuild b/examples/ota_https/main/Kconfig.projbuild new file mode 100644 index 00000000..4a94e3c8 --- /dev/null +++ b/examples/ota_https/main/Kconfig.projbuild @@ -0,0 +1,11 @@ +menu "Example Configuration" + + config EXAMPLE_WIFI_SSID + string "WiFi SSID for connection" + default "EXAMPLE_SSID" + + config EXAMPLE_WIFI_PASSWORD + string "WiFi Password for connection" + default "EXAMPLE_PASSWORD" + +endmenu diff --git a/examples/ota_https/main/app_main.c b/examples/ota_https/main/app_main.c new file mode 100644 index 00000000..0bcdbe50 --- /dev/null +++ b/examples/ota_https/main/app_main.c @@ -0,0 +1,116 @@ +/* OTA HTTPS Example + + 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 +#include +#include + +#include +#include +#include +#include +#include +#include + +static const char *TAG = "app_mmain"; + +#define WIFI_SSID CONFIG_EXAMPLE_WIFI_SSID +#define WIFI_PASSWORD CONFIG_EXAMPLE_WIFI_PASSWORD +#define WIFI_RESET_BUTTON_TIMEOUT 3 + +static EventGroupHandle_t g_wifi_event_group; +#define WIFI_CONNECTED_BIT BIT0 + +static void event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGW(TAG, "WiFi disconnected. Trying to connect."); + esp_wifi_connect(); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "WiFi connected, got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + xEventGroupSetBits(g_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +esp_err_t wifi_init_connect(void) +{ + g_wifi_event_group = xEventGroupCreate(); + + esp_err_t err; + err = esp_event_loop_create_default(); + if (err == ESP_ERR_INVALID_STATE){ + ESP_LOGW(TAG, "Event loop creation failed. Continuing since it might be created elsewhere"); + } else if (err != ESP_OK) { + ESP_LOGE(TAG, "ESP Event Loop creation failed."); + return ESP_FAIL; + }; + ESP_ERROR_CHECK(esp_netif_init()); + + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, + ESP_EVENT_ANY_ID, + &event_handler, + NULL)); + + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, + ESP_EVENT_ANY_ID, + &event_handler, + NULL)); + + wifi_config_t wifi_config = { + .sta = { + .ssid = WIFI_SSID, + .password = WIFI_PASSWORD + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_ERROR_CHECK(esp_wifi_connect()); + + xEventGroupWaitBits(g_wifi_event_group, + WIFI_CONNECTED_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); + + return ESP_OK; +} + +void app_main() +{ + esp_err_t err = esp_event_loop_create_default(); + ESP_ERROR_CHECK(err); + + err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + + /* Initialize factory parition for HTTPS OTA */ + err = esp_rmaker_factory_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize rmaker factory partition."); + } + ESP_ERROR_CHECK(err); + + ESP_ERROR_CHECK(wifi_init_connect()); + + // Needs to be done after WiFi is connected. + esp_rmaker_ota_https_enable(NULL); + esp_rmaker_ota_https_fetch(); +} \ No newline at end of file diff --git a/examples/ota_https/partitions.csv b/examples/ota_https/partitions.csv new file mode 100644 index 00000000..5c9166b2 --- /dev/null +++ b/examples/ota_https/partitions.csv @@ -0,0 +1,10 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table +esp_secure_cert, 0x3F, , 0xD000, 0x2000, encrypted +nvs_key, data, nvs_keys, 0xF000, 0x1000, encrypted +nvs, data, nvs, 0x10000, 0x6000, +otadata, data, ota, , 0x2000 +phy_init, data, phy, , 0x1000, +ota_0, app, ota_0, 0x20000, 1600K, +ota_1, app, ota_1, , 1600K, +fctry, data, nvs, 0x340000, 0x6000 diff --git a/examples/ota_https/partitions_4mb_optimised.csv b/examples/ota_https/partitions_4mb_optimised.csv new file mode 100644 index 00000000..6c337712 --- /dev/null +++ b/examples/ota_https/partitions_4mb_optimised.csv @@ -0,0 +1,11 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table +esp_secure_cert, 0x3F, , 0xD000, 0x2000, encrypted +nvs_key, data, nvs_keys, 0xF000, 0x1000, encrypted +nvs, data, nvs, 0x10000, 0x6000, +otadata, data, ota, , 0x2000 +phy_init, data, phy, , 0x1000, +ota_0, app, ota_0, 0x20000, 0x1E0000, +ota_1, app, ota_1, 0x200000, 0x1E0000, +reserved, 0x06, , 0x3E0000, 0x1A000, +fctry, data, nvs, 0x3FA000, 0x6000 diff --git a/examples/ota_https/sdkconfig.defaults b/examples/ota_https/sdkconfig.defaults new file mode 100644 index 00000000..033ceeb6 --- /dev/null +++ b/examples/ota_https/sdkconfig.defaults @@ -0,0 +1,24 @@ +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# +# Use partition table which makes use of flash to the fullest +# Can be used for other platforms as well. But please keep in mind that fctry partition address is +# different than default, and the new address needs to be specified to `rainmaker.py claim` +# +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb_optimised.csv" + +# mbedtls +CONFIG_MBEDTLS_DYNAMIC_BUFFER=y +CONFIG_MBEDTLS_DYNAMIC_FREE_PEER_CERT=y +CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y + +# Temporary Fix for Timer Overflows +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=3120 + +# For additional security on reset to factory +CONFIG_ESP_RMAKER_USER_ID_CHECK=y + +# Application Rollback +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y \ No newline at end of file diff --git a/examples/ota_https/sdkconfig.defaults.esp32 b/examples/ota_https/sdkconfig.defaults.esp32 new file mode 100644 index 00000000..c308d070 --- /dev/null +++ b/examples/ota_https/sdkconfig.defaults.esp32 @@ -0,0 +1 @@ +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y diff --git a/examples/ota_https/sdkconfig.defaults.esp32c2 b/examples/ota_https/sdkconfig.defaults.esp32c2 new file mode 100644 index 00000000..e1cc0d98 --- /dev/null +++ b/examples/ota_https/sdkconfig.defaults.esp32c2 @@ -0,0 +1,121 @@ +# Bluetooth +CONFIG_BT_ENABLED=y +CONFIG_BT_RELEASE_IRAM=y +CONFIG_BT_NIMBLE_ENABLED=y + +## NimBLE Options +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_BONDS=2 +CONFIG_BT_NIMBLE_MAX_CCCDS=2 +CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=3072 +CONFIG_BT_NIMBLE_ROLE_CENTRAL=n +CONFIG_BT_NIMBLE_ROLE_OBSERVER=n +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=10 +CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=100 +CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=4 +CONFIG_BT_NIMBLE_ACL_BUF_COUNT=5 +CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=5 +CONFIG_BT_NIMBLE_HCI_EVT_LO_BUF_COUNT=3 +CONFIG_BT_NIMBLE_GATT_MAX_PROCS=1 +CONFIG_BT_NIMBLE_ENABLE_CONN_REATTEMPT=n +CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n +CONFIG_BT_NIMBLE_WHITELIST_SIZE=1 +## Controller Options +CONFIG_BT_LE_CONTROLLER_TASK_STACK_SIZE=3072 +CONFIG_BT_LE_LL_RESOLV_LIST_SIZE=1 +CONFIG_BT_LE_LL_DUP_SCAN_LIST_COUNT=1 + +# SPI Configuration +CONFIG_SPI_MASTER_ISR_IN_IRAM=n +CONFIG_SPI_SLAVE_ISR_IN_IRAM=n + +# Ethernet +CONFIG_ETH_USE_SPI_ETHERNET=n + +# Event Loop Library +CONFIG_ESP_EVENT_POST_FROM_ISR=n + +# Chip revision +CONFIG_ESP32C2_REV2_DEVELOPMENT=y + +# ESP Ringbuf +CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH=y +CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=y + +# ESP System Settings +CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=16 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2048 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=3072 + +# Bypass a bug. Use 26M XTAL Freq +CONFIG_XTAL_FREQ_26=y + +## Memory protection +CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT=n + +# High resolution timer (esp_timer) +CONFIG_ESP_TIMER_TASK_STACK_SIZE=2048 + +# Wi-Fi +CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=n +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=3 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=6 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=6 +CONFIG_ESP32_WIFI_IRAM_OPT=n +CONFIG_ESP32_WIFI_RX_IRAM_OPT=n +CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=n +CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=n +CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n + +# FreeRTOS +## Kernel +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +## Port +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=n +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y +CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH=y + +# Hardware Abstraction Layer (HAL) and Low Level (LL) +CONFIG_HAL_ASSERTION_DISABLE=y + +# LWIP +CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=16 +CONFIG_LWIP_DHCPS=n +CONFIG_LWIP_IPV6_AUTOCONFIG=y +CONFIG_LWIP_MAX_ACTIVE_TCP=5 +CONFIG_LWIP_MAX_LISTENING_TCP=5 +CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=n +CONFIG_LWIP_TCP_SYNMAXRTX=12 +CONFIG_LWIP_TCP_MSL=40000 +CONFIG_LWIP_TCP_FIN_WAIT_TIMEOUT=16000 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=4096 +CONFIG_LWIP_TCP_WND_DEFAULT=2440 +CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS=y +CONFIG_LWIP_TCP_RTO_TIME=1500 +CONFIG_LWIP_MAX_UDP_PCBS=8 +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=2560 +CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT=y +CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT=y + +# mbedTLS +CONFIG_MBEDTLS_DYNAMIC_BUFFER=y +CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y +CONFIG_MBEDTLS_DYNAMIC_FREE_CA_CERT=y +CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=n +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y + +# SPI Flash driver +CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=n +CONFIG_SPI_FLASH_ROM_IMPL=y + +# Websocket +CONFIG_WS_TRANSPORT=n + +# Virtual file system +CONFIG_VFS_SUPPORT_DIR=n +CONFIG_VFS_SUPPORT_SELECT=n +CONFIG_VFS_SUPPORT_TERMIOS=n + +# Wear Levelling +CONFIG_WL_SECTOR_SIZE_512=y diff --git a/examples/ota_https/sdkconfig.defaults.esp32c6 b/examples/ota_https/sdkconfig.defaults.esp32c6 new file mode 100644 index 00000000..0caf64ce --- /dev/null +++ b/examples/ota_https/sdkconfig.defaults.esp32c6 @@ -0,0 +1,10 @@ +# +# Use partition table which makes use of flash to the fullest +# Can be used for other platforms as well. But please keep in mind that fctry partition address is +# different than default, and the new address needs to be specified to `rainmaker.py claim` +# +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb_optimised.csv" + +# To accomodate security features +CONFIG_PARTITION_TABLE_OFFSET=0xc000 diff --git a/examples/ota_https/sdkconfig.defaults.esp32h2 b/examples/ota_https/sdkconfig.defaults.esp32h2 new file mode 100644 index 00000000..f53ae72b --- /dev/null +++ b/examples/ota_https/sdkconfig.defaults.esp32h2 @@ -0,0 +1,14 @@ +# Enable OpenThread +CONFIG_OPENTHREAD_ENABLED=y +CONFIG_OPENTHREAD_CLI=n + +# Enable DNS64 client and Network connection resolve hook +CONFIG_OPENTHREAD_DNS64_CLIENT=y +CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT=y + +# Increase network provisioning scan entries +CONFIG_NETWORK_PROV_SCAN_MAX_ENTRIES=64 + +# Use 4MB optimised partition +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb_optimised.csv" diff --git a/examples/ota_https/sdkconfig.defaults.esp32s2 b/examples/ota_https/sdkconfig.defaults.esp32s2 new file mode 100644 index 00000000..9a2f0e8c --- /dev/null +++ b/examples/ota_https/sdkconfig.defaults.esp32s2 @@ -0,0 +1,4 @@ +# +# Bluetooth +# +CONFIG_BT_ENABLED=n