diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml new file mode 100644 index 0000000..22b20f0 --- /dev/null +++ b/.github/workflows/auto-tag.yml @@ -0,0 +1,102 @@ +name: Auto Tag from Component Version + +on: + push: + branches: [ master ] + paths: + - 'components/esp_rainmaker/idf_component.yml' + + # Allow manual trigger + workflow_dispatch: + +permissions: + contents: write # Required to create tags + +jobs: + auto-tag: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Get full history for tagging + + - name: Extract version from component file + id: get_version + run: | + # Extract version from YAML file using grep and sed + VERSION=$(grep '^version:' components/esp_rainmaker/idf_component.yml | sed 's/version: *"\(.*\)"/\1/') + + if [ -z "$VERSION" ]; then + echo "Error: Could not extract version from idf_component.yml" + exit 1 + fi + + # Validate version format (basic semver check) + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$'; then + echo "Error: Invalid version format: $VERSION" + exit 1 + fi + + echo "Extracted version: $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag_name=v$VERSION" >> $GITHUB_OUTPUT + + - name: Check if tag already exists + id: check_tag + run: | + TAG_NAME="v${{ steps.get_version.outputs.version }}" + + # Check if tag exists locally + if git tag -l | grep -q "^$TAG_NAME$"; then + echo "Tag $TAG_NAME already exists locally" + echo "tag_exists=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check if tag exists on remote + if git ls-remote --tags origin | grep -q "refs/tags/$TAG_NAME$"; then + echo "Tag $TAG_NAME already exists on remote" + echo "tag_exists=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Tag $TAG_NAME does not exist" + echo "tag_exists=false" >> $GITHUB_OUTPUT + + - name: Create and push tag + if: steps.check_tag.outputs.tag_exists == 'false' + run: | + TAG_NAME="v${{ steps.get_version.outputs.version }}" + + # Configure git user (use GitHub Actions bot) + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Create annotated tag + git tag -a "$TAG_NAME" -m "Release $TAG_NAME - Auto-tagged from idf_component.yml" + + # Push tag to origin + git push origin "$TAG_NAME" + + echo "✅ Successfully created and pushed tag: $TAG_NAME" + + - name: Tag already exists + if: steps.check_tag.outputs.tag_exists == 'true' + run: | + TAG_NAME="v${{ steps.get_version.outputs.version }}" + echo "ℹ️ Tag $TAG_NAME already exists. Skipping tag creation." + + - name: Summary + run: | + echo "## Auto-Tag Summary" >> $GITHUB_STEP_SUMMARY + echo "- **Component:** esp_rainmaker" >> $GITHUB_STEP_SUMMARY + echo "- **Version:** ${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Tag:** ${{ steps.get_version.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.check_tag.outputs.tag_exists }}" == "true" ]; then + echo "- **Status:** ⚠️ Tag already exists" >> $GITHUB_STEP_SUMMARY + else + echo "- **Status:** ✅ Tag created successfully" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/components/esp_rainmaker/CHANGELOG.md b/components/esp_rainmaker/CHANGELOG.md index fd45741..24bc78f 100644 --- a/components/esp_rainmaker/CHANGELOG.md +++ b/components/esp_rainmaker/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## 1.6.4 + +## New feature + +- Support for OTA over MQTT: The regular OTA upgrades with ESP RainMaker required an +additional https connection to fetch the OTA image, which has a significant impact on +heap usage. With MQTT OTA, the same MQTT channel used for rest of the RainMaker +communication is used to fetch the OTA image, thereby saving on RAM. +This could be slightly slower and may odd some cost. but the overall +impact would be low enough when compared against the advantages. This can be enabled by setting +`CONFIG_ESP_RMAKER_OTA_USE_MQTT` to `y` in the menuconfig. +(`idf.py menuconfig -> ESP RainMaker Config -> ESP RainMaker OTA Config -> OTA Update Protocol Type -> MQTT`) + +## Bugfix + +- Fix a bug where the OTA fetch was not working when `CONFIG_ESP_RMAKER_OTA_AUTOFETCH` was enabled. + + +## 1.6.3 + +## Bugfix + +- Duplicate otafetch after rebooting into new firmware after an OTA was causing a crash. +This was seen when both, CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE and CONFIG_ESP_RMAKER_OTA_AUTOFETCH +are enabled. + +## 1.6.2 + +## Minor changes + +- AVoid execution of `esp_rmaker_params_mqtt_init()` from esp-idf event handler + +## 1.6.1 + +## Minor changes + +- Make cmd response payload publish api (`esp_rmaker_cmd_response_publish()`) public + ## 1.6.0 ### Enhancements @@ -9,4 +47,4 @@ - Add retry logic if otafetch fails - Add OTA retry on failure mechanism - Try OTA multiple times (as per `CONFIG_ESP_RMAKER_OTA_MAX_RETRIES`, set to 3 by default) if it fails - - Schedule an OTA fetch as per `CONFIG_ESP_RMAKER_OTA_RETRY_DELAY_MINUTES` if all retries fail \ No newline at end of file + - Schedule an OTA fetch as per `CONFIG_ESP_RMAKER_OTA_RETRY_DELAY_MINUTES` if all retries fail diff --git a/components/esp_rainmaker/CMakeLists.txt b/components/esp_rainmaker/CMakeLists.txt index 064dd07..0f00009 100644 --- a/components/esp_rainmaker/CMakeLists.txt +++ b/components/esp_rainmaker/CMakeLists.txt @@ -20,6 +20,11 @@ set(priv_req protobuf-c json_parser json_generator nvs_flash esp_http_client app_update esp-tls mbedtls esp_https_ota console esp_local_ctrl esp_https_server mdns esp_schedule efuse driver rmaker_common wifi_provisioning) +# Add CBOR for MQTT OTA support +if (CONFIG_ESP_RMAKER_OTA_USE_MQTT) + list(APPEND priv_req cbor) +endif() + if ("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.0") list(APPEND priv_req esp_app_format) endif() @@ -57,6 +62,12 @@ set(ota_srcs "src/ota/esp_rmaker_ota.c" "src/ota/esp_rmaker_ota_using_params.c" "src/ota/esp_rmaker_ota_using_topics.c") set(ota_priv_includes "src/ota") +if (CONFIG_ESP_RMAKER_OTA_USE_MQTT) + list(APPEND ota_srcs "src/ota/esp_rmaker_mqtt_ota.c") +endif() +if (CONFIG_ESP_RMAKER_OTA_USE_HTTPS) + list(APPEND ota_srcs "src/ota/esp_rmaker_https_ota.c") +endif() # Thread BR set(thread_br_srcs ) diff --git a/components/esp_rainmaker/Kconfig.projbuild b/components/esp_rainmaker/Kconfig.projbuild index b4f7c27..46eba7d 100644 --- a/components/esp_rainmaker/Kconfig.projbuild +++ b/components/esp_rainmaker/Kconfig.projbuild @@ -308,15 +308,6 @@ menu "ESP RainMaker Config" help This allows you to skip the project name check. - config ESP_RMAKER_OTA_HTTP_RX_BUFFER_SIZE - int "OTA HTTP receive buffer size" - default 1024 - range 512 LWIP_TCP_WND_DEFAULT - help - Increasing this value beyond the default would speed up the OTA download process. - However, please ensure that your application has enough memory headroom to allow this, - else, the OTA may fail. - config ESP_RMAKER_OTA_ROLLBACK_WAIT_PERIOD int "OTA Rollback Wait Period (Seconds)" default 90 @@ -374,6 +365,54 @@ menu "ESP RainMaker Config" help Delay (in minutes) before re-fetching OTA details after all retry attempts fail (for OTA using topics). + choice ESP_RMAKER_OTA_TYPE + prompt "OTA Update Protocol Type" + default ESP_RMAKER_OTA_USE_HTTPS + help + Protocol to be used for OTA Update + config ESP_RMAKER_OTA_USE_HTTPS + bool "HTTPS" + config ESP_RMAKER_OTA_USE_MQTT + bool "MQTT" + endchoice + + config ESP_RMAKER_OTA_HTTP_RX_BUFFER_SIZE + int "OTA HTTP receive buffer size" + default 1024 + range 512 LWIP_TCP_WND_DEFAULT + depends on ESP_RMAKER_OTA_USE_HTTPS + help + Increasing this value beyond the default would speed up the OTA download process. + However, please ensure that your application has enough memory headroom to allow this, + else, the OTA may fail. + + config ESP_RMAKER_MQTT_OTA_BLOCK_SIZE + int "MQTT OTA Block Length" + default 3072 + depends on ESP_RMAKER_OTA_USE_MQTT + range 256 131072 + help + The block size to fetch in a MQTT OTA publish message. + Note that number of blocks * block size should not exceed 128KB. + + config ESP_RMAKER_MQTT_OTA_NO_OF_BLOCKS + int "MQTT OTA No. of Blocks" + default 42 + depends on ESP_RMAKER_OTA_USE_MQTT + range 1 512 + help + The number of blocks of file to fetch in a MQTT OTA publish message. + Note that number of blocks * block size should not exceed 128KB. + + config ESP_RMAKER_MQTT_OTA_MAX_RETRIES + int "MQTT OTA Maximum Number of Retries" + default 3 + depends on ESP_RMAKER_OTA_USE_MQTT + range 1 255 + help + The number of times we should resend request for fetching file blocks, in case we do not get any response. + + endmenu menu "ESP RainMaker Scheduling" diff --git a/components/esp_rainmaker/idf_component.yml b/components/esp_rainmaker/idf_component.yml index 2930664..dc26a0c 100644 --- a/components/esp_rainmaker/idf_component.yml +++ b/components/esp_rainmaker/idf_component.yml @@ -1,5 +1,5 @@ ## IDF Component Manager Manifest File -version: "1.6.3" +version: "1.6.4" description: ESP RainMaker firmware agent url: https://github.com/espressif/esp-rainmaker/tree/master/components/esp_rainmaker repository: https://github.com/espressif/esp-rainmaker.git @@ -32,3 +32,5 @@ dependencies: version: "^1.2.0" rules: - if: "idf_version >= 5.1" + espressif/cbor: + version: "~0.6" diff --git a/components/esp_rainmaker/include/esp_rmaker_ota.h b/components/esp_rainmaker/include/esp_rmaker_ota.h index e7a9355..14c03e6 100644 --- a/components/esp_rainmaker/include/esp_rmaker_ota.h +++ b/components/esp_rainmaker/include/esp_rmaker_ota.h @@ -70,6 +70,8 @@ typedef void *esp_rmaker_ota_handle_t; typedef struct { /** The OTA URL received from ESP RainMaker Cloud */ char *url; + /** The Stream ID received from ESP RainMaker Cloud. This will be used only in MQTT based OTA*/ + char *stream_id; /** Size of the OTA File. Can be 0 if the file size isn't received from * the ESP RainMaker Cloud */ int filesize; @@ -177,6 +179,32 @@ typedef struct { void *priv; } esp_rmaker_ota_config_t; +/** HTTPS OTA Callback + * + * This callback forces the use of HTTPS protocol for OTA updates. + * Use this in your ota_config.ota_cb to always use HTTPS. + * + * @param[in] handle An OTA handle assigned by the ESP RainMaker Core + * @param[in] ota_data The data to be used for the OTA + * + * @return ESP_OK if the OTA was successful + * @return ESP_FAIL if the OTA failed + */ +esp_err_t esp_rmaker_ota_https_cb(esp_rmaker_ota_handle_t handle, esp_rmaker_ota_data_t *ota_data); + +/** MQTT OTA Callback + * + * This callback forces the use of MQTT protocol for OTA updates. + * Use this in your ota_config.ota_cb to always use MQTT. + * + * @param[in] handle An OTA handle assigned by the ESP RainMaker Core + * @param[in] ota_data The data to be used for the OTA + * + * @return ESP_OK if the OTA was successful + * @return ESP_FAIL if the OTA failed + */ +esp_err_t esp_rmaker_ota_mqtt_cb(esp_rmaker_ota_handle_t handle, esp_rmaker_ota_data_t *ota_data); + /** Enable OTA * * Calling this API enables OTA as per the ESP RainMaker specification. diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_https_ota.c b/components/esp_rainmaker/src/ota/esp_rmaker_https_ota.c new file mode 100644 index 0000000..ba69b2d --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_https_ota.c @@ -0,0 +1,232 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#if CONFIG_BT_ENABLED +#include +#endif /* CONFIG_BT_ENABLED */ + +#ifdef CONFIG_ESP_RMAKER_USE_CERT_BUNDLE +#include +#endif + +#include +#include +#include "esp_rmaker_internal.h" +#include "esp_rmaker_ota_internal.h" +#include "esp_rmaker_https_ota.h" + +static const char *TAG = "esp_rmaker_https_ota"; + +/* HTTPS-specific certificate definition */ +extern const char esp_rmaker_ota_def_cert[] asm("_binary_rmaker_ota_server_crt_start"); +const char *ESP_RMAKER_OTA_DEFAULT_SERVER_CERT = esp_rmaker_ota_def_cert; + +#define DEF_HTTP_TX_BUFFER_SIZE 1024 +#ifdef CONFIG_ESP_RMAKER_OTA_HTTP_RX_BUFFER_SIZE +#define DEF_HTTP_RX_BUFFER_SIZE CONFIG_ESP_RMAKER_OTA_HTTP_RX_BUFFER_SIZE +#else +#define DEF_HTTP_RX_BUFFER_SIZE 1024 +#endif + +/* Local macro for OTA max retries, can be overridden for development */ +#ifndef ESP_RMAKER_OTA_MAX_RETRIES +#define ESP_RMAKER_OTA_MAX_RETRIES CONFIG_ESP_RMAKER_OTA_MAX_RETRIES +#endif + +/* Local macro for OTA retry delay in seconds, can be overridden for development */ +#ifndef ESP_RMAKER_OTA_RETRY_DELAY_SECONDS +#define ESP_RMAKER_OTA_RETRY_DELAY_SECONDS (CONFIG_ESP_RMAKER_OTA_RETRY_DELAY_MINUTES * 60) +#endif + +#define ESP_RMAKER_HTTPS_OTA_TIMEOUT_MS 5000 + +static esp_err_t esp_rmaker_ota_use_https(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data, char *err_desc, size_t err_desc_size) +{ + int buffer_size_tx = DEF_HTTP_TX_BUFFER_SIZE; + /* In case received url is longer, we will increase the tx buffer size + * to accomodate the longer url and other headers. + */ + if (strlen(ota_data->url) > buffer_size_tx) { + buffer_size_tx = strlen(ota_data->url) + 128; + } + + if (ota_data->filesize) { + ESP_LOGD(TAG, "Received file size: %d", ota_data->filesize); + } + + esp_err_t ota_finish_err = ESP_OK; + esp_http_client_config_t config = { + .timeout_ms = ESP_RMAKER_HTTPS_OTA_TIMEOUT_MS, + .url = ota_data->url, +#ifdef CONFIG_ESP_RMAKER_USE_CERT_BUNDLE + .crt_bundle_attach = esp_crt_bundle_attach, +#else + .cert_pem = ota_data->server_cert ? ota_data->server_cert : ESP_RMAKER_OTA_DEFAULT_SERVER_CERT, +#endif +#ifdef CONFIG_ESP_RMAKER_SKIP_COMMON_NAME_CHECK + .skip_cert_common_name_check = true, +#endif + .buffer_size = DEF_HTTP_RX_BUFFER_SIZE, + .buffer_size_tx = buffer_size_tx, + .keep_alive_enable = true + }; + + esp_https_ota_config_t ota_config = { + .http_config = &config, + }; + /* Using a warning just to highlight the message */ + ESP_LOGW(TAG, "Starting OTA. This may take time."); + esp_https_ota_handle_t https_ota_handle = NULL; + esp_err_t err = esp_https_ota_begin(&ota_config, &https_ota_handle); + if (err != ESP_OK) { + int err_no = errno; + snprintf(err_desc, err_desc_size, "OTA Begin failed: %s (errno=%d: %s)", esp_err_to_name(err), err_no, err_no ? strerror(err_no) : "Invalid"); + return ESP_FAIL; + } + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +/* Get the current Wi-Fi power save type. In case OTA fails and we need this + * to restore power saving. + */ + wifi_ps_type_t ps_type; + esp_wifi_get_ps(&ps_type); +/* Disable Wi-Fi power save to speed up OTA, iff BT is controller is idle/disabled. + * Co-ex requirement, device panics otherwise.*/ +#if CONFIG_BT_ENABLED + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + esp_wifi_set_ps(WIFI_PS_NONE); + } +#else + esp_wifi_set_ps(WIFI_PS_NONE); +#endif /* CONFIG_BT_ENABLED */ +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + + esp_app_desc_t app_desc; + err = esp_https_ota_get_img_desc(https_ota_handle, &app_desc); + + if (err != ESP_OK) { + int err_no = errno; + snprintf(err_desc, err_desc_size, "Failed to read image description: %s (errno=%d: %s)", esp_err_to_name(err), err_no, err_no ? strerror(err_no) : "Invalid"); + /* OTA failed, may retry later */ + goto ota_end; + } + err = validate_image_header(ota_handle, &app_desc); + if (err != ESP_OK) { + snprintf(err_desc, err_desc_size, "Image header verification failed"); + /* OTA should be rejected, returning ESP_ERR_INVALID_STATE */ + err = ESP_ERR_INVALID_STATE; + goto ota_end; + } + + /* Report status: Downloading Firmware Image */ + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Downloading Firmware Image"); + + int count = 0; +#ifdef CONFIG_ESP_RMAKER_OTA_PROGRESS_SUPPORT + int last_ota_progress = 0; +#endif + while (1) { + err = esp_https_ota_perform(https_ota_handle); + if (err == ESP_ERR_INVALID_VERSION) { + snprintf(err_desc, err_desc_size, "Chip revision mismatch"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_REJECTED, err_desc); + /* OTA should be rejected, returning ESP_ERR_INVALID_STATE */ + err = ESP_ERR_INVALID_STATE; + goto ota_end; + } + if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) { + break; + } + /* esp_https_ota_perform returns after every read operation which gives user the ability to + * monitor the status of OTA upgrade by calling esp_https_ota_get_image_len_read, which gives length of image + * data read so far. + * We are using a counter just to reduce the number of prints + */ + count++; + if (count == 50) { + ESP_LOGI(TAG, "Image bytes read: %d", esp_https_ota_get_image_len_read(https_ota_handle)); + count = 0; + } +#ifdef CONFIG_ESP_RMAKER_OTA_PROGRESS_SUPPORT + int image_size = esp_https_ota_get_image_size(https_ota_handle); + int read_size = esp_https_ota_get_image_len_read(https_ota_handle); + int ota_progress = 100 * read_size / image_size; // The unit is % + /* When ota_progress is 0 or 100, we will not report the progress, beacasue the 0 and 100 is reported by additional_info `Downloading Firmware Image` and + * `Firmware Image download complete`. And every progress will only report once and the progress is increasing. + */ + if (((ota_progress != 0) && (ota_progress != 100)) && (ota_progress % CONFIG_ESP_RMAKER_OTA_PROGRESS_INTERVAL == 0) && (last_ota_progress < ota_progress)) { + last_ota_progress = ota_progress; + char description[40] = {0}; + snprintf(description, sizeof(description), "Downloaded %d%% Firmware Image", ota_progress); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, description); + } +#endif + } + if (err != ESP_OK) { + int err_no = errno; + snprintf(err_desc, err_desc_size, "OTA failed: %s (errno=%d: %s)", esp_err_to_name(err), err_no, err_no ? strerror(err_no) : "Invalid"); + /* OTA failed, may retry later */ + goto ota_end; + } + + if (esp_https_ota_is_complete_data_received(https_ota_handle) != true) { + snprintf(err_desc, err_desc_size, "Complete data was not received"); + /* OTA failed, may retry later */ + err = ESP_FAIL; + goto ota_end; + } + + /* Report completion before finishing */ + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Firmware Image download complete"); + +ota_end: +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#ifdef CONFIG_BT_ENABLED + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + esp_wifi_set_ps(ps_type); + } +#else + esp_wifi_set_ps(ps_type); +#endif /* CONFIG_BT_ENABLED */ +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + + if (err == ESP_OK) { + /* Success path: finish the OTA */ + ota_finish_err = esp_https_ota_finish(https_ota_handle); + if (ota_finish_err == ESP_OK) { + return ESP_OK; + } else if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { + snprintf(err_desc, err_desc_size, "Image validation failed"); + } else { + int err_no = errno; + snprintf(err_desc, err_desc_size, "OTA finish failed: %s (errno=%d: %s)", esp_err_to_name(ota_finish_err), err_no, err_no ? strerror(err_no) : "Invalid"); + } + /* Handle already closed by esp_https_ota_finish(), don't call abort */ + return ESP_FAIL; + } + + /* Error path: abort the OTA */ + esp_https_ota_abort(https_ota_handle); + return (err == ESP_ERR_INVALID_STATE) ? ESP_ERR_INVALID_STATE : ESP_FAIL; +} + +esp_err_t esp_rmaker_ota_https_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) +{ + if (!ota_data->url) { + return ESP_FAIL; + } + + /* Use the common OTA workflow with HTTPS-specific function */ + return esp_rmaker_ota_start_workflow(ota_handle, ota_data, esp_rmaker_ota_use_https, "HTTPS"); +} diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_https_ota.h b/components/esp_rainmaker/src/ota/esp_rmaker_https_ota.h new file mode 100644 index 0000000..4235eb3 --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_https_ota.h @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef CONFIG_ESP_RMAKER_OTA_USE_HTTPS + +/** + * @brief HTTPS OTA callback function + * + * This function handles HTTPS OTA update with retry logic and status reporting. + * + * @param[in] ota_handle The OTA handle + * @param[in] ota_data The OTA data containing URL and other information + * + * @return ESP_OK on success + * @return ESP_FAIL on failure + */ +esp_err_t esp_rmaker_ota_https_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data); + +#endif /* CONFIG_ESP_RMAKER_OTA_USE_HTTPS */ + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_mqtt_ota.c b/components/esp_rainmaker/src/ota/esp_rmaker_mqtt_ota.c new file mode 100644 index 0000000..4e49eae --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_mqtt_ota.c @@ -0,0 +1,1102 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * AWS IoT Over-the-air Update v3.4.0 + * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Additional includes for high-level MQTT OTA functions */ +#include +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#include +#endif +#ifdef CONFIG_BT_ENABLED +#include +#endif +#include "esp_rmaker_ota_internal.h" + +#ifndef MIN +#define MAX(a, b) ({ \ + __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; \ +}) + +#define MIN(a, b) ({ \ + __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a < _b ? _a : _b; \ +}) +#endif + +#define WAIT_FOR_DATA_SEC 10 +#define LOG2_BITS_PER_BYTE 3U +#define BITS_PER_BYTE ((uint32_t)1U << LOG2_BITS_PER_BYTE) +#define OTA_ERASED_BLOCKS_VAL 0xffU +#define IMAGE_HEADER_SIZE sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t) + 1 + +// AWS MQTT File Delivery does not allow to fetch more than 128kb of data in a single request +#define MQTT_OTA_MAX_BYTES_PER_REQUEST (128*1024) + #if (CONFIG_ESP_RMAKER_MQTT_OTA_NO_OF_BLOCKS * CONFIG_ESP_RMAKER_MQTT_OTA_BLOCK_SIZE) > MQTT_OTA_MAX_BYTES_PER_REQUEST + #error Total block size per request should not exceed 128kb + #endif + +static const char *TAG = "esp_rmaker_mqtt_ota"; + +/* Add global counter for progress reporting */ +static int mqtt_ota_block_count = 0; + +typedef struct { + int current_offset; + int remaining_size; + int current_block_length; + int current_no_of_blocks; + uint32_t blocks_remaining; /*!< @brief How many blocks remain to be received (a code optimization). */ + uint8_t *block_bitmap; /*!< @brief Bitmap to track of received blocks. */ + int32_t bitmap_len; + int file_id; + int stream_version; + int bytes_read; +} esp_rmaker_mqtt_file_params_t; + +typedef struct { + esp_ota_handle_t update_handle; + const esp_partition_t *update_partition; + char *stream_id; + uint8_t *ota_upgrade_buf; + size_t ota_upgrade_buf_size; + int binary_file_len; + int image_length; + esp_rmaker_mqtt_ota_state state; + char *image_header_buf; + esp_rmaker_mqtt_file_params_t *file_fetch_params; + uint8_t retry_count; + uint8_t max_retries; + /* Progress reporting fields */ + void (*progress_cb)(int bytes_read, int total_bytes, void *priv); + void *progress_priv; + int last_reported_progress; +} esp_rmaker_mqtt_ota_t; + +typedef struct { + uint64_t stream_version; + uint8_t file_id; + uint32_t length; + uint32_t offset; + uint32_t no_of_blocks; + uint8_t *block_bitmap; + int32_t bitmap_len; +} get_stream_req_t; + +typedef struct { + uint8_t *payload; + size_t payload_len; + int32_t file_id; + int32_t block_number; + int32_t length; +} get_stream_res_t; + +static EventGroupHandle_t mqtt_ota_event_group; + +/* MQTT OTA uses CBOR encoding for optimized size and performance */ +#include "cbor.h" +#define MQTT_FILE_DELIVERY_TOPIC_SUFFIX "cbor" + +static CborError cbor_check_data_type(CborType expected_type, const CborValue *cbor_value) +{ + CborType actual_type = cbor_value_get_type(cbor_value); + return (actual_type != expected_type) ? CborErrorIllegalType : CborNoError; +} + +static esp_err_t decode_cbor_message(const uint8_t *message_buf, size_t message_size, + int32_t *file_id, int32_t *block_id, int32_t *block_size, + uint8_t *const *payload, size_t *payload_size) +{ + CborParser cbor_parser; + CborValue cbor_value, cborMap; + size_t payload_size_received = 0; + + if ((file_id == NULL) || (block_id == NULL) || (block_size == NULL) || + (payload == NULL) || (payload_size == NULL) || (message_buf == NULL)) { + return ESP_ERR_INVALID_ARG; + } + /* Initialize the parser. */ + if (cbor_parser_init(message_buf, message_size, 0, &cbor_parser, &cborMap) != CborNoError) { + return ESP_FAIL; + } + + /* Get the outer element and confirm that it's a "map," i.e., a set of + * CBOR key/value pairs. */ + if (cbor_value_is_map(&cborMap) == false) { + return ESP_FAIL; + } + + /* Find the file ID. */ + if (cbor_value_map_find_value(&cborMap, "f", &cbor_value) != CborNoError) { + return ESP_FAIL; + } + if (cbor_check_data_type(CborIntegerType, &cbor_value) != CborNoError) { + return ESP_FAIL; + } + if (cbor_value_get_int(&cbor_value, (int *)file_id) != CborNoError) { + return ESP_FAIL; + } + + /* Find the block ID. */ + if (cbor_value_map_find_value(&cborMap, "i", &cbor_value) != CborNoError) { + return ESP_FAIL; + } + if (cbor_check_data_type(CborIntegerType, &cbor_value) != CborNoError) { + return ESP_FAIL; + } + if (cbor_value_get_int(&cbor_value, (int *)block_id) != CborNoError) { + return ESP_FAIL; + } + + /* Find the block size. */ + if (cbor_value_map_find_value(&cborMap, "l", &cbor_value) != CborNoError) { + return ESP_FAIL; + } + if (cbor_check_data_type(CborIntegerType, &cbor_value) != CborNoError) { + return ESP_FAIL; + } + if (cbor_value_get_int(&cbor_value, (int *)block_size) != CborNoError) { + return ESP_FAIL; + } + + /* Find the payload bytes. */ + if (cbor_value_map_find_value(&cborMap, "p", &cbor_value) != CborNoError) { + return ESP_FAIL; + } + if (cbor_check_data_type(CborByteStringType, &cbor_value) != CborNoError) { + return ESP_FAIL; + } + + /* Calculate the size we need to malloc for the payload. */ + if (cbor_value_calculate_string_length(&cbor_value, &payload_size_received) != CborNoError) { + return ESP_FAIL; + } + + /* Check if the received payload size is less than or equal to buffer size. */ + if (payload_size_received <= (*payload_size)) { + *payload_size = payload_size_received; + } else { + return ESP_FAIL; + } + + if (cbor_value_copy_byte_string(&cbor_value, *payload, payload_size, NULL) != CborNoError) { + return ESP_FAIL; + } + + return ESP_OK; +} + +static esp_err_t encode_cbor_message(uint8_t *message_buf, size_t msg_buf_size, size_t *encoded_msg_size, + int32_t file_id, int32_t block_size, int32_t block_offset, + const uint8_t *block_bitmap, size_t block_bitmap_size, int32_t num_of_blocks_requested) +{ + CborEncoder cbor_encoder, cbor_map_encoder; + + if ((message_buf == NULL) || (encoded_msg_size == NULL)) { + return ESP_ERR_INVALID_ARG; + } + + size_t item_count = 5; + if (block_bitmap == NULL) { + item_count = 4; + } + + /* Initialize the CBOR encoder. */ + cbor_encoder_init(&cbor_encoder, message_buf, msg_buf_size, 0); + if (cbor_encoder_create_map(&cbor_encoder, &cbor_map_encoder, item_count) != CborNoError) { + return ESP_FAIL; + } + + /* Encode the file ID key and value. */ + if (cbor_encode_text_stringz(&cbor_map_encoder, "f") != CborNoError) { + return ESP_FAIL; + } + if (cbor_encode_int(&cbor_map_encoder, file_id) != CborNoError) { + return ESP_FAIL; + } + + /* Encode the block size key and value. */ + if (cbor_encode_text_stringz(&cbor_map_encoder, "l") != CborNoError) { + return ESP_FAIL; + } + if (cbor_encode_int(&cbor_map_encoder, block_size) != CborNoError) { + return ESP_FAIL; + } + + /* Encode the block offset key and value. */ + if (cbor_encode_text_stringz(&cbor_map_encoder, "o") != CborNoError) { + return ESP_FAIL; + } + if (cbor_encode_int(&cbor_map_encoder, block_offset) != CborNoError) { + return ESP_FAIL; + } + + /* Encode the block bitmap key and value. */ + if (block_bitmap != NULL) { + if (cbor_encode_text_stringz(&cbor_map_encoder, "b") != CborNoError) { + return ESP_FAIL; + } + if (cbor_encode_byte_string(&cbor_map_encoder, block_bitmap, block_bitmap_size) != CborNoError) { + return ESP_FAIL; + } + } + + /* Encode the number of blocks requested key and value. */ + if (cbor_encode_text_stringz(&cbor_map_encoder, "n") != CborNoError) { + return ESP_FAIL; + } + if (cbor_encode_int(&cbor_map_encoder, num_of_blocks_requested) != CborNoError) { + return ESP_FAIL; + } + + /* Close the encoder. */ + if (cbor_encoder_close_container_checked(&cbor_encoder, &cbor_map_encoder) != CborNoError) { + return ESP_FAIL; + } + + /* Get the encoded size. */ + *encoded_msg_size = cbor_encoder_get_buffer_size(&cbor_encoder, message_buf); + + return ESP_OK; +} + +static esp_err_t create_get_stream_data_request(uint8_t *buf, size_t len, size_t *encoded_size, const get_stream_req_t *req) +{ + if (!buf || !encoded_size || !req || (len <= 0)) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t ret = encode_cbor_message(buf, len, encoded_size, req->file_id, req->length, req->offset, + (uint8_t *)req->block_bitmap, req->bitmap_len, req->no_of_blocks); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Error in encoding cbor message."); + } + return ret; +} + +static esp_err_t create_get_stream_data_response(uint8_t *buf, size_t len, get_stream_res_t *res, esp_rmaker_mqtt_ota_t *handle) +{ + if (!buf || (len <= 0) || !res || !handle) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t ret = decode_cbor_message(buf, len, &res->file_id, &res->block_number, + &res->length, &(res->payload), &res->payload_len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Error in decoding cbor response."); + } + return ret; +} + + + +static bool _received_complete_response(esp_rmaker_mqtt_ota_t *handle, const get_stream_req_t *req) +{ + for (int i = req->offset; i < (req->offset + req->no_of_blocks); i++) { + int32_t byte = i >> LOG2_BITS_PER_BYTE; + // Check if block received is duplicate + if (((handle->file_fetch_params->block_bitmap[ byte ] >> (i % BITS_PER_BYTE)) & (int8_t)0x01U) != 0) { + return false; + } + } + return true; +} + +static esp_err_t _ota_write(esp_rmaker_mqtt_ota_t *mqtt_ota_handle, const void *buffer, size_t buf_len, size_t offset) +{ + if (buffer == NULL || mqtt_ota_handle == NULL || (buf_len <= 0)) { + ESP_LOGE(TAG, "_ota_write: Invalid arguments."); + return ESP_FAIL; + } + esp_err_t err = esp_ota_write_with_offset(mqtt_ota_handle->update_handle, buffer, buf_len, offset); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error: esp_ota_write failed! err=0x%x", err); + } else { + mqtt_ota_handle->binary_file_len += buf_len; + } + return err; +} + +static void error_cb(const char *topic, void *payload, size_t payload_len, void *priv_data) +{ + char *output = payload; + ESP_LOGE(TAG, "Received error response on topic : %s", topic); + ESP_LOGD(TAG, "Data received : %.*s", payload_len, output); + ESP_LOGD(TAG, "Length of received data : %d", payload_len); + xEventGroupSetBits(mqtt_ota_event_group, FILE_BLOCK_FETCH_ERR); +} + +static void stream_data_cb(const char *topic, void *payload, size_t payload_len, void *priv_data) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)priv_data; + if (!handle || (handle->state > ESP_MQTT_OTA_IN_PROGRESS) || (handle->state < ESP_MQTT_OTA_BEGIN)) { + return; + } + esp_rmaker_mqtt_file_params_t *file_fetch_params = handle->file_fetch_params; + if (!file_fetch_params) { + return; + } + get_stream_res_t response_data; + int32_t byte; + int8_t bit_mask; + response_data.payload = handle->ota_upgrade_buf; + response_data.payload_len = file_fetch_params->current_block_length; + esp_err_t err = create_get_stream_data_response(payload, payload_len, &response_data, handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to decode response."); + return; + } + // check duplicate blocks when bitmap is not used + if (response_data.block_number < handle->file_fetch_params->current_offset || + response_data.block_number >= handle->file_fetch_params->current_offset + handle->file_fetch_params->current_no_of_blocks) { + ESP_LOGD(TAG, "Received duplicate block. Discarding..."); + return; + } + switch (handle->state) { + case ESP_MQTT_OTA_BEGIN: + if (response_data.block_number == 0 && response_data.payload_len >= IMAGE_HEADER_SIZE && handle->image_header_buf != NULL) { + memcpy(handle->image_header_buf, handle->ota_upgrade_buf, IMAGE_HEADER_SIZE); + } + xEventGroupSetBits(mqtt_ota_event_group, FILE_BLOCK_FETCHED); + return; + case ESP_MQTT_OTA_IN_PROGRESS: + byte = response_data.block_number >> LOG2_BITS_PER_BYTE; + bit_mask = (uint8_t)(1U << (response_data.block_number % BITS_PER_BYTE)); + // Check if block received is duplicate + if (((file_fetch_params->block_bitmap[ byte ] >> (response_data.block_number % BITS_PER_BYTE)) & (int8_t)0x01U) == 0) { + ESP_LOGI(TAG, "Duplicate Block Received."); + xEventGroupSetBits(mqtt_ota_event_group, FILE_BLOCK_DUPLICATE); + return; + } + // Update Bitmap + file_fetch_params->block_bitmap[byte] &= (uint8_t)((uint8_t) 0xFFU & (~bit_mask)); + + file_fetch_params->blocks_remaining -= 1; + file_fetch_params->bytes_read += response_data.payload_len; + file_fetch_params->remaining_size -= response_data.payload_len; + + if (_ota_write(handle, handle->ota_upgrade_buf, response_data.payload_len, + response_data.block_number * file_fetch_params->current_block_length) == ESP_OK) { + + /* Call progress callback if set */ + if (handle->progress_cb) { + handle->progress_cb(handle->binary_file_len, handle->image_length, handle->progress_priv); + } + + /* Log progress occasionally for debugging */ + mqtt_ota_block_count++; + if (mqtt_ota_block_count % 20 == 0) { + ESP_LOGI(TAG, "Image bytes read: %d", handle->binary_file_len); + } + + xEventGroupSetBits(mqtt_ota_event_group, FILE_BLOCK_FETCHED); + } else { + xEventGroupSetBits(mqtt_ota_event_group, FILE_BLOCK_FETCH_ERR); + } + return; + default: + ESP_LOGE(TAG, "Invalid OTA State: %d. Discarding block...", handle->state); + } +} + +static esp_err_t esp_rmaker_fetch_block(esp_rmaker_mqtt_ota_t *handle, get_stream_req_t *req) +{ + char publish_topic[100]; + snprintf(publish_topic, sizeof(publish_topic), "$aws/things/%s/streams/%s/get/%s", + esp_rmaker_get_node_id(), handle->stream_id, MQTT_FILE_DELIVERY_TOPIC_SUFFIX); + uint8_t publish_payload[200]; + size_t encoded_size = 0; + esp_err_t err = create_get_stream_data_request(publish_payload, sizeof(publish_payload), &encoded_size, req); + if (err != ESP_OK) { + return ESP_FAIL; + } + err = esp_rmaker_mqtt_publish(publish_topic, publish_payload, encoded_size, RMAKER_MQTT_QOS1, NULL); + return err; +} + +static esp_err_t esp_rmaker_mqtt_fetch_file(esp_rmaker_mqtt_ota_t *handle) +{ + EventBits_t uxBits; + esp_rmaker_mqtt_file_params_t *file_fetch_params = handle->file_fetch_params; + esp_err_t err; + int blocks_received = 0; + get_stream_req_t req = { + .stream_version = file_fetch_params->stream_version, + .file_id = file_fetch_params->file_id, + .offset = file_fetch_params->current_offset, + .length = file_fetch_params->current_block_length, + .no_of_blocks = file_fetch_params->current_no_of_blocks, + .block_bitmap = NULL, // To pass bitmap with the request, pass pointer to bitmap. + .bitmap_len = file_fetch_params->bitmap_len + }; + err = esp_rmaker_fetch_block(handle, &req); + if (err != ESP_OK) { + return err; + } + while (blocks_received < file_fetch_params->current_no_of_blocks) { + uxBits = xEventGroupWaitBits(mqtt_ota_event_group, FILE_BLOCK_FETCHED | FILE_BLOCK_FETCH_ERR | FILE_BLOCK_DUPLICATE, + pdTRUE, pdFALSE, (WAIT_FOR_DATA_SEC * 1000) / portTICK_PERIOD_MS); + if ((uxBits & FILE_BLOCK_FETCHED) != 0) { + blocks_received++; + handle->retry_count = 0; + } else if ((uxBits & FILE_BLOCK_DUPLICATE) != 0) { + blocks_received++; + } else if ((uxBits & FILE_BLOCK_FETCH_ERR) != 0) { + err = ESP_FAIL; + break; + } else { // Timeout + ESP_LOGE(TAG, "Request timed out."); + if (handle->retry_count < handle->max_retries) { + handle->retry_count += 1; + err = ESP_OK; + break; + } else { + ESP_LOGE(TAG, "Out of retries. Aborting OTA..."); + err = ESP_ERR_TIMEOUT; + break; + } + } + } + if (err == ESP_OK) { + if (_received_complete_response(handle, &req) == true) { + file_fetch_params->current_offset += file_fetch_params->current_no_of_blocks; + file_fetch_params->current_no_of_blocks = + MIN(handle->file_fetch_params->blocks_remaining, file_fetch_params->current_no_of_blocks); + } + } + return err; +} + + +static esp_err_t esp_rmaker_mqtt_subscribe_to_stream_topics(esp_rmaker_mqtt_ota_t *handle) +{ + esp_err_t err; + char *node_id = esp_rmaker_get_node_id(); + char subscribe_topic[100]; + snprintf(subscribe_topic, sizeof(subscribe_topic), "$aws/things/%s/streams/%s/rejected/%s", + node_id, handle->stream_id, MQTT_FILE_DELIVERY_TOPIC_SUFFIX); + if ((err = esp_rmaker_mqtt_subscribe(subscribe_topic, error_cb, RMAKER_MQTT_QOS1, handle->file_fetch_params)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to subscribe to %s", subscribe_topic); + } + snprintf(subscribe_topic, sizeof(subscribe_topic), "$aws/things/%s/streams/%s/data/%s", + node_id, handle->stream_id, MQTT_FILE_DELIVERY_TOPIC_SUFFIX); + if ((err = esp_rmaker_mqtt_subscribe(subscribe_topic, stream_data_cb, RMAKER_MQTT_QOS1, handle)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to subscribe to %s", subscribe_topic); + } + return err; +} + +static esp_err_t read_header(esp_rmaker_mqtt_ota_t *handle) +{ + if (!handle) { + return ESP_ERR_INVALID_ARG; + } + if (handle->image_header_buf) { + return ESP_OK; + } + get_stream_req_t req = { + .stream_version = 1, + .offset = 0, + .no_of_blocks = 1, + .length = IMAGE_HEADER_SIZE, + .file_id = handle->file_fetch_params->file_id, + .block_bitmap = NULL, + .bitmap_len = 0 + }; + handle->image_header_buf = MEM_CALLOC_EXTRAM(1, IMAGE_HEADER_SIZE); + if (!handle->image_header_buf) { + ESP_LOGE(TAG, "Failed to allocate memory to image header data buffer"); + return ESP_ERR_NO_MEM; + } + if (esp_rmaker_fetch_block(handle, &req) != ESP_OK) { + return ESP_FAIL; + } + EventBits_t uxBits = xEventGroupWaitBits(mqtt_ota_event_group, FILE_BLOCK_FETCHED | FILE_BLOCK_FETCH_ERR, + pdTRUE, pdFALSE, (WAIT_FOR_DATA_SEC * 1000) / portTICK_PERIOD_MS); + if ((uxBits & FILE_BLOCK_FETCHED) != 0) { + return ESP_OK; + } + return ESP_FAIL; +} + +static esp_err_t esp_ota_verify_chip_id(void *arg) +{ + esp_image_header_t *data = (esp_image_header_t*)(arg); + if (data->chip_id != CONFIG_IDF_FIRMWARE_CHIP_ID) { + ESP_LOGE(TAG, "Mismatch chip id, expected %d, found %d", CONFIG_IDF_FIRMWARE_CHIP_ID, data->chip_id); + return ESP_ERR_INVALID_VERSION; + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_mqtt_unsubscribe_stream_topics(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (!handle) { + return ESP_ERR_INVALID_ARG; + } + char topic[100] = {0}; + char *node_id = esp_rmaker_get_node_id(); + esp_err_t err = ESP_OK; + snprintf(topic, sizeof(topic), "$aws/things/%s/streams/%s/rejected/%s", + node_id, handle->stream_id, MQTT_FILE_DELIVERY_TOPIC_SUFFIX); + if ((err = esp_rmaker_mqtt_unsubscribe(topic)) != ESP_OK) { + ESP_LOGW(TAG, "Failed to unsubscribe from %s", topic); + } + snprintf(topic, sizeof(topic), "$aws/things/%s/streams/%s/data/%s", + node_id, handle->stream_id, MQTT_FILE_DELIVERY_TOPIC_SUFFIX); + if ((err = esp_rmaker_mqtt_unsubscribe(topic)) != ESP_OK) { + ESP_LOGW(TAG, "Failed to unsubscribe from %s", topic); + } + return err; +} + + +esp_err_t esp_mqtt_ota_begin(esp_rmaker_mqtt_ota_config_t *config, esp_rmaker_mqtt_ota_handle_t *handle) +{ + esp_err_t err; + /* Create event group only if it doesn't exist to prevent memory leak */ + if (mqtt_ota_event_group == NULL) { + mqtt_ota_event_group = xEventGroupCreate(); + if (mqtt_ota_event_group == NULL) { + ESP_LOGE(TAG, "Failed to create MQTT OTA event group"); + return ESP_ERR_NO_MEM; + } + } + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + + /* Reset progress counter for new OTA session */ + mqtt_ota_block_count = 0; + + esp_rmaker_mqtt_ota_t *mqtt_ota_handle = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_mqtt_ota_t)); + + if (!mqtt_ota_handle) { + ESP_LOGE(TAG, "Failed to allocate memory to ota handle"); + *handle = NULL; + return ESP_ERR_NO_MEM; + } + + mqtt_ota_handle->file_fetch_params = MEM_CALLOC_EXTRAM(1, sizeof(esp_rmaker_mqtt_file_params_t)); + if (!mqtt_ota_handle->file_fetch_params) { + ESP_LOGE(TAG, "Failed to allocate memory to file fetch parameters"); + err = ESP_ERR_NO_MEM; + goto mqtt_cleanup; + } + + mqtt_ota_handle->stream_id = config->stream_id; + err = esp_rmaker_mqtt_subscribe_to_stream_topics(mqtt_ota_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to subscribe to topics"); + goto mqtt_cleanup; + } + + mqtt_ota_handle->image_length = (int)config->filesize; + mqtt_ota_handle->max_retries = config->max_retry_count; + + /* Initialize progress reporting fields */ + mqtt_ota_handle->progress_cb = NULL; + mqtt_ota_handle->progress_priv = NULL; + mqtt_ota_handle->last_reported_progress = 0; + + ESP_LOGI(TAG, "Image length = %d bytes.", mqtt_ota_handle->image_length); + mqtt_ota_handle->update_partition = NULL; + ESP_LOGI(TAG, "Starting OTA..."); + + mqtt_ota_handle->update_partition = esp_ota_get_next_update_partition(NULL); + if (mqtt_ota_handle->update_partition == NULL) { + ESP_LOGE(TAG, "Passive OTA partition not found."); + err = ESP_FAIL; + goto mqtt_cleanup; + } + + ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%"PRIx32, + mqtt_ota_handle->update_partition->subtype, mqtt_ota_handle->update_partition->address); + + const int alloc_size = MAX(config->block_length, IMAGE_HEADER_SIZE); + mqtt_ota_handle->ota_upgrade_buf = (uint8_t *)MEM_ALLOC_EXTRAM(alloc_size); + if (!mqtt_ota_handle->ota_upgrade_buf) { + ESP_LOGE(TAG, "Failed to allocate memory to OTA upgrade data buffer."); + err = ESP_ERR_NO_MEM; + goto mqtt_cleanup; + } + mqtt_ota_handle->image_header_buf = NULL; + mqtt_ota_handle->ota_upgrade_buf_size = alloc_size; + mqtt_ota_handle->binary_file_len = 0; + + mqtt_ota_handle->file_fetch_params->stream_version = 1; + mqtt_ota_handle->file_fetch_params->file_id = 1; + mqtt_ota_handle->file_fetch_params->current_block_length = config->block_length; + mqtt_ota_handle->file_fetch_params->current_offset = 0; + mqtt_ota_handle->file_fetch_params->current_no_of_blocks = config->blocks_per_request; + mqtt_ota_handle->file_fetch_params->remaining_size = mqtt_ota_handle->image_length; + + + uint8_t *block_bitmap = NULL; + uint32_t file_size = mqtt_ota_handle->image_length; + uint32_t num_blocks = (file_size / config->block_length) + ((file_size % config->block_length > 0) ? 1 : 0); + uint32_t bitmap_len = (num_blocks + (BITS_PER_BYTE - 1)) >> LOG2_BITS_PER_BYTE; + uint8_t bit = 1U << (BITS_PER_BYTE - 1U); + uint32_t num_out_of_range = (bitmap_len * BITS_PER_BYTE) - num_blocks; + + block_bitmap = MEM_ALLOC_EXTRAM(bitmap_len); + if (!block_bitmap) { + ESP_LOGE(TAG, "Failed to allocate memory to bitmap."); + err = ESP_ERR_NO_MEM; + goto mqtt_cleanup; + } + /* Set all bits in the bitmap to the erased state (we use 1 for erased just like flash memory). */ + memset(block_bitmap, (int32_t)OTA_ERASED_BLOCKS_VAL, bitmap_len); + + uint32_t index; + for (index = 0; index < num_out_of_range; index++) { + block_bitmap[bitmap_len - 1] &= (uint8_t)((uint8_t)0xFFU & (~bit)); + bit >>= 1U; + } + mqtt_ota_handle->file_fetch_params->block_bitmap = block_bitmap; + mqtt_ota_handle->file_fetch_params->bitmap_len = bitmap_len; + mqtt_ota_handle->file_fetch_params->blocks_remaining = num_blocks; + *handle = (esp_rmaker_mqtt_ota_handle_t)mqtt_ota_handle; + mqtt_ota_handle->state = ESP_MQTT_OTA_BEGIN; + return ESP_OK; + +mqtt_cleanup: + if (mqtt_ota_handle) { + if (mqtt_ota_handle->file_fetch_params) { + free(mqtt_ota_handle->file_fetch_params); + mqtt_ota_handle->file_fetch_params = NULL; + } + if (mqtt_ota_handle->ota_upgrade_buf) { + free(mqtt_ota_handle->ota_upgrade_buf); + mqtt_ota_handle->ota_upgrade_buf = NULL; + } + free(mqtt_ota_handle); + mqtt_ota_handle = NULL; + } + *handle = NULL; + return err; +} + +esp_err_t esp_mqtt_ota_perform(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (handle == NULL) { + ESP_LOGE(TAG, "esp_mqtt_ota_perform: Invalid argument."); + return ESP_ERR_INVALID_ARG; + } + if (handle->state < ESP_MQTT_OTA_BEGIN) { + ESP_LOGE(TAG, "esp_mqtt_ota_perform: Invalid state."); + return ESP_ERR_INVALID_STATE; + } + esp_err_t err = ESP_OK; + switch(handle->state) { + case ESP_MQTT_OTA_BEGIN: + err = esp_ota_begin(handle->update_partition, handle->image_length, &handle->update_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err)); + handle->state = ESP_MQTT_OTA_FAILED; + return err; + } + if (read_header(handle) != ESP_OK) { + ESP_LOGE(TAG, "Failed to read header"); + handle->state = ESP_MQTT_OTA_FAILED; + return ESP_FAIL; + } + handle->binary_file_len = 0; + err = esp_ota_verify_chip_id(handle->image_header_buf); + if (handle->image_header_buf) { + free(handle->image_header_buf); + handle->image_header_buf = NULL; + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to verify chip ID."); + handle->state = ESP_MQTT_OTA_FAILED; + return err; + } + handle->file_fetch_params->bytes_read = 0; + handle->state = ESP_MQTT_OTA_IN_PROGRESS; + return err; + case ESP_MQTT_OTA_IN_PROGRESS: + err = esp_rmaker_mqtt_fetch_file(handle); + if (err != ESP_OK) { + handle->state = ESP_MQTT_OTA_FAILED; + return ESP_FAIL; + } + if (handle->file_fetch_params->bytes_read > 0) { + handle->file_fetch_params->bytes_read = 0; + if (handle->file_fetch_params->blocks_remaining == 0) { + ESP_LOGI(TAG, "Firmware Image fetched successfully."); + } + } + if (handle->image_length == handle->binary_file_len) { + handle->state = ESP_MQTT_OTA_SUCCESS; + } + break; + case ESP_MQTT_OTA_SUCCESS: + break; + case ESP_MQTT_OTA_FAILED: + break; + default: + return ESP_FAIL; + break; + } + return ESP_OK; +} + +static esp_err_t esp_mqtt_ota_end(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle, bool set_boot_partition) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (handle->state < ESP_MQTT_OTA_BEGIN) { + return ESP_FAIL; + } + esp_err_t err = ESP_OK; + esp_rmaker_mqtt_unsubscribe_stream_topics(mqtt_ota_handle); + switch(handle->state) { + case ESP_MQTT_OTA_FAILED: + /* fall through */ + case ESP_MQTT_OTA_SUCCESS: + /* fall through */ + case ESP_MQTT_OTA_IN_PROGRESS: + err = esp_ota_end(handle->update_handle); + /* fall through */ + case ESP_MQTT_OTA_BEGIN: + if (handle->ota_upgrade_buf) { + free(handle->ota_upgrade_buf); + handle->ota_upgrade_buf = NULL; + } + if (handle->file_fetch_params) { + if (handle->file_fetch_params->block_bitmap) { + free(handle->file_fetch_params->block_bitmap); + handle->file_fetch_params->block_bitmap = NULL; + } + free(handle->file_fetch_params); + handle->file_fetch_params = NULL; + } + if (handle->image_header_buf) { + free(handle->image_header_buf); + handle->image_header_buf = NULL; + } + vEventGroupDelete(mqtt_ota_event_group); + mqtt_ota_event_group = NULL; /* Set to NULL to prevent dangling pointer */ + break; + default: + ESP_LOGE(TAG, "Invalid ESP MQTT OTA State."); + break; + } + if (set_boot_partition && (err == ESP_OK) && (handle->state == ESP_MQTT_OTA_SUCCESS)) { + err = esp_ota_set_boot_partition(handle->update_partition); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed! err=0x%x", err); + } + } + free(handle); + handle = NULL; + return err; +} + +esp_err_t esp_mqtt_ota_finish(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle) +{ + return esp_mqtt_ota_end(mqtt_ota_handle, true); +} + +esp_err_t esp_mqtt_ota_abort(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle) +{ + return esp_mqtt_ota_end(mqtt_ota_handle, false); +} + +esp_rmaker_mqtt_ota_state esp_mqtt_ota_get_state(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (handle == NULL) { + return ESP_MQTT_OTA_INVALID_STATE; + } + return handle->state; +} + +int esp_mqtt_ota_get_image_len_read(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (handle == NULL) { + return 0; + } + return handle->binary_file_len; +} + +bool esp_mqtt_ota_is_complete_data_received(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (handle == NULL) { + ESP_LOGE(TAG, "esp_rmaker_mqtt_ota_is_complete_data_received: Invalid argument."); + return false; + } + return (handle->binary_file_len == handle->image_length); +} + +esp_err_t esp_mqtt_ota_get_img_desc(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle, esp_app_desc_t *new_app_info) +{ + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (handle == NULL || new_app_info == NULL) { + ESP_LOGE(TAG, "esp_mqtt_ota_get_img_desc: Invalid argument."); + return ESP_ERR_INVALID_ARG; + } + if (handle->state < ESP_MQTT_OTA_BEGIN) { + ESP_LOGE(TAG, "esp_mqtt_ota_get_img_desc: Invalid state."); + return ESP_FAIL; + } + if (read_header(handle) != ESP_OK) { + return ESP_FAIL; + } + + /* Ensure we have valid header buffer */ + if (!handle->image_header_buf) { + ESP_LOGE(TAG, "Image header buffer not available"); + return ESP_FAIL; + } + + /* Validate buffer bounds before accessing app descriptor */ + size_t offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t); + if (offset + sizeof(esp_app_desc_t) > IMAGE_HEADER_SIZE) { + ESP_LOGE(TAG, "App descriptor extends beyond header size"); + return ESP_FAIL; + } + + /* Use fresh header data from image_header_buf instead of potentially stale ota_upgrade_buf */ + memcpy(new_app_info, &handle->image_header_buf[offset], sizeof(esp_app_desc_t)); + return ESP_OK; +} + +/* Constants specific to MQTT OTA */ +#ifndef ESP_RMAKER_OTA_RETRY_DELAY_SECONDS +#define ESP_RMAKER_OTA_RETRY_DELAY_SECONDS (CONFIG_ESP_RMAKER_OTA_RETRY_DELAY_MINUTES * 60) +#endif + +/* Local macro for OTA max retries, can be overridden for development */ +#ifndef ESP_RMAKER_OTA_MAX_RETRIES +#define ESP_RMAKER_OTA_MAX_RETRIES CONFIG_ESP_RMAKER_OTA_MAX_RETRIES +#endif + +#ifdef CONFIG_ESP_RMAKER_OTA_PROGRESS_SUPPORT +static int last_ota_progress = 0; +static void mqtt_ota_progress_cb(int bytes_read, int total_bytes, void *priv) +{ + esp_rmaker_ota_handle_t ota_handle = (esp_rmaker_ota_handle_t)priv; + if (ota_handle && total_bytes > 0) { + int ota_progress = 100 * bytes_read / total_bytes; /* The unit is % */ + /* When ota_progress is 0 or 100, we will not report the progress, because the 0 and 100 is reported by additional_info `Downloading Firmware Image` and + * `Firmware Image download complete`. And every progress will only report once and the progress is increasing. + */ + if (((ota_progress != 0) && (ota_progress != 100)) && (ota_progress % CONFIG_ESP_RMAKER_OTA_PROGRESS_INTERVAL == 0) && (last_ota_progress < ota_progress)) { + last_ota_progress = ota_progress; + char description[40] = {0}; + snprintf(description, sizeof(description), "Downloaded %d%% Firmware Image", ota_progress); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, description); + } + } +} +esp_err_t esp_mqtt_ota_set_progress_cb(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle, void (*progress_cb)(int bytes_read, int total_bytes, void *priv), void *priv) +{ + last_ota_progress = 0; + esp_rmaker_mqtt_ota_t *handle = (esp_rmaker_mqtt_ota_t *)mqtt_ota_handle; + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + handle->progress_cb = progress_cb; + handle->progress_priv = priv; + return ESP_OK; +} +#endif + +static esp_err_t esp_rmaker_ota_use_mqtt(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data, char *err_desc, size_t err_desc_size) { + if (!ota_data->stream_id || !ota_data->filesize) { + snprintf(err_desc, err_desc_size, "Missing stream_id or filesize for MQTT OTA"); + return ESP_FAIL; + } + + esp_err_t ota_finish_err = ESP_OK; + esp_rmaker_mqtt_ota_config_t ota_config = { + .stream_id = ota_data->stream_id, + .filesize = ota_data->filesize, + .block_length = CONFIG_ESP_RMAKER_MQTT_OTA_BLOCK_SIZE, + .blocks_per_request = CONFIG_ESP_RMAKER_MQTT_OTA_NO_OF_BLOCKS, + .max_retry_count = CONFIG_ESP_RMAKER_MQTT_OTA_MAX_RETRIES + }; + esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle = NULL; + if (ota_data->filesize) { + ESP_LOGD(TAG, "Received file size: %d", ota_data->filesize); + } + + /* Using a warning just to highlight the message */ + ESP_LOGW(TAG, "Starting OTA. This may take time."); + esp_err_t err = esp_mqtt_ota_begin(&ota_config, &mqtt_ota_handle); + if (err != ESP_OK) { + snprintf(err_desc, err_desc_size, "ESP MQTT OTA Begin failed: %s", esp_err_to_name(err)); + return ESP_FAIL; + } + + /* Set up progress reporting callback */ +#ifdef CONFIG_ESP_RMAKER_OTA_PROGRESS_SUPPORT + esp_mqtt_ota_set_progress_cb(mqtt_ota_handle, mqtt_ota_progress_cb, ota_handle); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +/* Get the current Wi-Fi power save type. In case OTA fails and we need this + * to restore power saving. + */ + wifi_ps_type_t ps_type; + esp_wifi_get_ps(&ps_type); +/* Disable Wi-Fi power save to speed up OTA, iff BT is controller is idle/disabled. + * Co-ex requirement, device panics otherwise.*/ +#if CONFIG_BT_ENABLED + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + esp_wifi_set_ps(WIFI_PS_NONE); + } +#else + esp_wifi_set_ps(WIFI_PS_NONE); +#endif /* CONFIG_BT_ENABLED */ +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + + esp_app_desc_t app_desc; + err = esp_mqtt_ota_get_img_desc(mqtt_ota_handle, &app_desc); + if (err != ESP_OK) { + snprintf(err_desc, err_desc_size, "Failed to read image description: %s", esp_err_to_name(err)); + /* OTA failed, may retry later */ + goto ota_end; + } + err = validate_image_header(ota_handle, &app_desc); + if (err != ESP_OK) { + snprintf(err_desc, err_desc_size, "Image header verification failed"); + /* OTA should be rejected, returning ESP_ERR_INVALID_STATE */ + err = ESP_ERR_INVALID_STATE; + goto ota_end; + } + /* Report status: Downloading Firmware Image */ + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Downloading Firmware Image"); + + int count = 0; + + while (1) { + err = esp_mqtt_ota_perform(mqtt_ota_handle); + if (err == ESP_ERR_INVALID_VERSION) { + snprintf(err_desc, err_desc_size, "Chip revision mismatch"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_REJECTED, err_desc); + /* OTA should be rejected, returning ESP_ERR_INVALID_STATE */ + err = ESP_ERR_INVALID_STATE; + goto ota_end; + } + if (err != ESP_OK && esp_mqtt_ota_get_state(mqtt_ota_handle) != ESP_MQTT_OTA_SUCCESS) { + if (err != ESP_ERR_TIMEOUT) { /* Don't break on timeout, just continue */ + break; + } + } + if (esp_mqtt_ota_get_state(mqtt_ota_handle) == ESP_MQTT_OTA_SUCCESS) { + err = ESP_OK; + break; + } + + /* esp_mqtt_ota_perform returns after every read operation which gives user the ability to + * monitor the status of OTA upgrade by calling esp_mqtt_ota_get_image_len_read, which gives length of image + * data read so far. + * We are using a counter just to reduce the number of prints + */ + count++; + if (count == 50) { + ESP_LOGI(TAG, "Image bytes read: %d", esp_mqtt_ota_get_image_len_read(mqtt_ota_handle)); + count = 0; + } + } + if (err != ESP_OK) { + snprintf(err_desc, err_desc_size, "OTA failed: %s", esp_err_to_name(err)); + /* OTA failed, may retry later */ + goto ota_end; + } + + if (esp_mqtt_ota_is_complete_data_received(mqtt_ota_handle) != true) { + snprintf(err_desc, err_desc_size, "Complete data was not received"); + /* OTA failed, may retry later */ + err = ESP_FAIL; + goto ota_end; + } + + /* Report completion before finishing */ + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Firmware Image download complete"); + +ota_end: +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#ifdef CONFIG_BT_ENABLED + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + esp_wifi_set_ps(ps_type); + } +#else + esp_wifi_set_ps(ps_type); +#endif /* CONFIG_BT_ENABLED */ +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + + if (err == ESP_OK) { + /* Success path: finish the OTA */ + ota_finish_err = esp_mqtt_ota_finish(mqtt_ota_handle); + if (ota_finish_err == ESP_OK) { + return ESP_OK; + } else if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { + snprintf(err_desc, err_desc_size, "Image validation failed"); + } else { + snprintf(err_desc, err_desc_size, "OTA finish failed: %s", esp_err_to_name(ota_finish_err)); + } + /* Handle already closed by esp_mqtt_ota_finish(), don't call abort */ + return ESP_FAIL; + } + + /* Error path: abort the OTA */ + esp_mqtt_ota_abort(mqtt_ota_handle); + return (err == ESP_ERR_INVALID_STATE) ? ESP_ERR_INVALID_STATE : ESP_FAIL; +} + +esp_err_t esp_rmaker_ota_mqtt_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) +{ + if (!ota_data->stream_id || !ota_data->filesize) { + return ESP_FAIL; + } + + /* Use the common OTA workflow with MQTT-specific function */ + return esp_rmaker_ota_start_workflow(ota_handle, ota_data, esp_rmaker_ota_use_mqtt, "MQTT"); +} diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_mqtt_ota.h b/components/esp_rainmaker/src/ota/esp_rmaker_mqtt_ota.h new file mode 100644 index 0000000..8bb7f3c --- /dev/null +++ b/components/esp_rainmaker/src/ota/esp_rmaker_mqtt_ota.h @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif +typedef enum { + FILE_BLOCK_FETCHED = 1, + FILE_BLOCK_DUPLICATE = 2, + FILE_FETCH_COMPLETE = 4, + FILE_BLOCK_FETCH_ERR = 8, +} esp_rmaker_mqtt_events; + +typedef enum { + ESP_MQTT_OTA_INVALID_STATE = -1, + ESP_MQTT_OTA_INIT, + ESP_MQTT_OTA_BEGIN, + ESP_MQTT_OTA_IN_PROGRESS, + ESP_MQTT_OTA_SUCCESS, + ESP_MQTT_OTA_FAILED, +} esp_rmaker_mqtt_ota_state; + +typedef struct { + char *stream_id; + uint32_t filesize; + uint32_t block_length; + uint16_t blocks_per_request; + uint8_t max_retry_count; +} esp_rmaker_mqtt_ota_config_t; + +typedef void *esp_rmaker_mqtt_ota_handle_t; + +/** Begin MQTT OTA Update + * + * This API allocates memory to the buffer and initializes the initial OTA state. + * + * @param[in] config MQTT OTA Configuration + * @param[out] handle MQTT OTA handle required by other MQTT OTA APIs + * + * @return ESP_OK if the MQTT OTA handle is created. + * @return error on failure + */ +esp_err_t esp_mqtt_ota_begin(esp_rmaker_mqtt_ota_config_t *config, esp_rmaker_mqtt_ota_handle_t *handle); + +/** Set progress callback for MQTT OTA Update + * + * This API sets a callback function that will be called during OTA progress + * to report the number of bytes downloaded and total file size. + * + * @param[in] mqtt_ota_handle MQTT OTA handle + * @param[in] progress_cb Progress callback function + * @param[in] priv Private data passed to callback function + * + * @return ESP_OK on success. + * @return error on failure + */ +esp_err_t esp_mqtt_ota_set_progress_cb(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle, void (*progress_cb)(int bytes_read, int total_bytes, void *priv), void *priv); + +/** Perform MQTT OTA Update file fetch operations. + * + * This API will fetch blocks of the OTA image as specified in the configuration. + * This API should be called repeatedly until all blocks are fetched or a failure occurs. + * + * @param[in] mqtt_handle MQTT OTA handle + * + * @return ESP_OK if the file fetch request was successful. + * @return error on failure + */ +esp_err_t esp_mqtt_ota_perform(esp_rmaker_mqtt_ota_handle_t mqtt_handle); + +/** Free the memory resources allocated for MQTT OTA and update the device boot partition. + * + * This API should be called after the OTA image was fetched successfully. + * + * @param[in] mqtt_handle MQTT OTA handle + * + * @return ESP_OK on success. + * @return error on failure + */ +esp_err_t esp_mqtt_ota_finish(esp_rmaker_mqtt_ota_handle_t mqtt_handle); + +/** Free the memory resources allocated for MQTT OTA. + * + * This API should be called to cancel the OTA update. + * + * @param[in] mqtt_ota_handle MQTT OTA handle + * + * @return ESP_OK on success. + * @return error on failure + */ +esp_err_t esp_mqtt_ota_abort(esp_rmaker_mqtt_ota_handle_t mqtt_handle); + +/** Get current OTA state + * + * @param[in] mqtt_ota_handle MQTT OTA handle + * + * @return state of MQTT OTA + * @return ESP_MQTT_OTA_INVALID_STATE if mqtt_ota_handle is NULL + */ +esp_rmaker_mqtt_ota_state esp_mqtt_ota_get_state(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle); + +/** Get downloaded length of OTA image + * + * @param[in] mqtt_ota_handle MQTT OTA handle + * + * @return length of downloaded image + */ +int esp_mqtt_ota_get_image_len_read(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle); + +/** Find whether OTA image download is complete. + * + * @param[in] mqtt_ota_handle MQTT OTA handle + * + * @return true if complete OTA image is downloaded; false otherwise. + */ +bool esp_mqtt_ota_is_complete_data_received(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle); + +/** Read the OTA image header and retrieve its app description. + * + * @param[in] mqtt_ota_handle MQTT OTA handle + * @param[out] new_app_info Image description + * + * @return ESP_OK if the image description was fetched successfully. + * @return error on failure + */ +esp_err_t esp_mqtt_ota_get_img_desc(esp_rmaker_mqtt_ota_handle_t mqtt_ota_handle, esp_app_desc_t *new_app_info); + +/** + * @brief MQTT OTA callback function + * + * This function handles MQTT OTA update with retry logic and status reporting. + * It orchestrates the entire MQTT OTA process from start to finish. + * + * @param[in] ota_handle The OTA handle + * @param[in] ota_data The OTA data containing stream_id, filesize and other information + * + * @return ESP_OK on success + * @return ESP_FAIL on failure + */ +esp_err_t esp_rmaker_ota_mqtt_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_rainmaker/src/ota/esp_rmaker_ota.c b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c index 7bfd216..e358446 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota.c +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota.c @@ -1,16 +1,8 @@ -// 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. +/* + * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ #include #include @@ -21,11 +13,16 @@ #include #include #include -#include #include #include #include #include +#ifdef CONFIG_ESP_RMAKER_OTA_USE_HTTPS +#include "esp_rmaker_https_ota.h" +#endif +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT +#include "esp_rmaker_mqtt_ota.h" +#endif #if CONFIG_BT_ENABLED #include #endif /* CONFIG_BT_ENABLED */ @@ -37,6 +34,11 @@ #include "esp_rmaker_internal.h" #include "esp_rmaker_ota_internal.h" +/* Forward declarations for static functions */ +static esp_err_t esp_rmaker_ota_handle_metadata_common(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data); +static esp_err_t esp_rmaker_ota_success_reboot_sequence(esp_rmaker_ota_handle_t ota_handle, const char *protocol_name, int attempt_count); + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) // Features supported in 4.4+ @@ -54,24 +56,26 @@ #endif /* !IDF4.4 */ static const char *TAG = "esp_rmaker_ota"; +/* OTA reboot timer and NVS constants */ #define OTA_REBOOT_TIMER_SEC 10 -#define DEF_HTTP_TX_BUFFER_SIZE 1024 -#define DEF_HTTP_RX_BUFFER_SIZE CONFIG_ESP_RMAKER_OTA_HTTP_RX_BUFFER_SIZE +#define ESP_RMAKER_NVS_PART_NAME "nvs" +#define RMAKER_OTA_UPDATE_FLAG_NVS_NAME "ota_update" + +/* OTA retry constants */ +#ifndef ESP_RMAKER_OTA_MAX_RETRIES +#define ESP_RMAKER_OTA_MAX_RETRIES CONFIG_ESP_RMAKER_OTA_MAX_RETRIES +#endif +#ifndef ESP_RMAKER_OTA_RETRY_DELAY_SECONDS +#define ESP_RMAKER_OTA_RETRY_DELAY_SECONDS (CONFIG_ESP_RMAKER_OTA_RETRY_DELAY_MINUTES * 60) +#endif + +/* Core OTA rollback functionality */ #define RMAKER_OTA_ROLLBACK_WAIT_PERIOD CONFIG_ESP_RMAKER_OTA_ROLLBACK_WAIT_PERIOD -extern const char esp_rmaker_ota_def_cert[] asm("_binary_rmaker_ota_server_crt_start"); -const char *ESP_RMAKER_OTA_DEFAULT_SERVER_CERT = esp_rmaker_ota_def_cert; + ESP_EVENT_DEFINE_BASE(RMAKER_OTA_EVENT); -typedef enum { - OTA_OK = 0, - OTA_ERR, - OTA_DELAYED -} esp_rmaker_ota_action_t; - static esp_rmaker_ota_t *g_ota_priv; -#ifdef CONFIG_ESP_RMAKER_OTA_PROGRESS_SUPPORT -static int last_ota_progress = 0; -#endif + char *esp_rmaker_ota_status_to_string(ota_status_t status) { @@ -112,7 +116,7 @@ esp_rmaker_ota_event_t esp_rmaker_ota_status_to_event(ota_status_t status) return RMAKER_OTA_EVENT_INVALID; } -static inline esp_err_t esp_rmaker_ota_post_event(esp_rmaker_event_t event_id, void* data, size_t data_size) +esp_err_t esp_rmaker_ota_post_event(esp_rmaker_event_t event_id, void *data, size_t data_size) { return esp_event_post(RMAKER_OTA_EVENT, event_id, data, data_size, portMAX_DELAY); } @@ -150,6 +154,9 @@ void esp_rmaker_ota_common_cb(void *priv) } esp_rmaker_ota_data_t ota_data = { .url = ota->url, +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT + .stream_id = ota->stream_id, +#endif .filesize = ota->filesize, .fw_version = ota->fw_version, .ota_job_id = (char *)ota->transient_priv, @@ -166,7 +173,7 @@ ota_finish: } } -static esp_err_t validate_image_header(esp_rmaker_ota_handle_t ota_handle, +esp_err_t validate_image_header(esp_rmaker_ota_handle_t ota_handle, esp_app_desc_t *new_app_info) { if (new_app_info == NULL) { @@ -207,6 +214,74 @@ static esp_err_t validate_image_header(esp_rmaker_ota_handle_t ota_handle, return ESP_OK; } +/* Common retry loop with configurable protocol-specific logic */ + +static esp_err_t esp_rmaker_ota_retry_loop(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data, + ota_protocol_func_t protocol_func, const char *protocol_name, int *attempt_count) +{ + esp_err_t err = ESP_FAIL; + char err_desc[128] = {0}; + int attempt; + + for (attempt = 0; attempt < ESP_RMAKER_OTA_MAX_RETRIES; ++attempt) { + /* Simplified status reporting - just say "OTA" for cleaner messages */ + char info[64]; + snprintf(info, sizeof(info), "Starting OTA Upgrade (%s attempt %d/%d)", protocol_name, attempt + 1, ESP_RMAKER_OTA_MAX_RETRIES); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, info); + ESP_LOGW(TAG, "Starting %s OTA attempt %d/%d. This may take time.", protocol_name, attempt + 1, ESP_RMAKER_OTA_MAX_RETRIES); + + err = protocol_func(ota_handle, ota_data, err_desc, sizeof(err_desc)); + if (err == ESP_OK) { + break; + } else if (err == ESP_ERR_INVALID_STATE) { + return ESP_FAIL; + } else { + ESP_LOGE(TAG, "%s OTA attempt %d failed: %s", protocol_name, attempt + 1, err_desc); + /* Simplified failure reporting */ + char fail_info[192]; + snprintf(fail_info, sizeof(fail_info), "OTA Attempt %d/%d failed: %s", attempt + 1, ESP_RMAKER_OTA_MAX_RETRIES, err_desc); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, fail_info); + } + } + + if (err != ESP_OK) { + /* Handle retry delay for topic-based OTA */ + esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; + if (ota->type == OTA_USING_TOPICS) { + esp_rmaker_ota_fetch_with_delay(ESP_RMAKER_OTA_RETRY_DELAY_SECONDS); + } + return ESP_FAIL; + } + + *attempt_count = attempt + 1; + return ESP_OK; +} + +/* Complete OTA workflow orchestration function */ + +esp_err_t esp_rmaker_ota_start_workflow(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data, + ota_protocol_func_t protocol_func, const char *protocol_name) +{ + /* Step 1: Handle metadata */ + esp_err_t metadata_result = esp_rmaker_ota_handle_metadata_common(ota_handle, ota_data); + if (metadata_result != ESP_OK) { + return metadata_result; + } + + /* Step 2: Post starting event */ + esp_rmaker_ota_post_event(RMAKER_OTA_EVENT_STARTING, NULL, 0); + + /* Step 3: Execute retry loop */ + int attempt_count = 0; + esp_err_t retry_result = esp_rmaker_ota_retry_loop(ota_handle, ota_data, protocol_func, protocol_name, &attempt_count); + if (retry_result != ESP_OK) { + return ESP_FAIL; + } + + /* Step 4: Handle success and reboot */ + return esp_rmaker_ota_success_reboot_sequence(ota_handle, protocol_name, attempt_count); +} + #ifdef CONFIG_ESP_RMAKER_OTA_TIME_SUPPORT /* Retry delay for cases wherein time info itself is not available */ @@ -319,10 +394,10 @@ esp_rmaker_ota_action_t esp_rmaker_ota_handle_time(jparse_ctx_t *jptr, esp_rmake #endif /* CONFIG_ESP_RMAKER_OTA_TIME_SUPPORT */ -esp_rmaker_ota_action_t esp_rmaker_ota_handle_metadata(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) +static esp_rmaker_ota_action_t esp_rmaker_ota_handle_metadata(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) { if (!ota_data->metadata) { - return ESP_OK; + return OTA_OK; } esp_rmaker_ota_action_t ota_action = OTA_OK; jparse_ctx_t jctx; @@ -336,6 +411,52 @@ esp_rmaker_ota_action_t esp_rmaker_ota_handle_metadata(esp_rmaker_ota_handle_t o return ota_action; } +/* Common OTA callback helper functions */ + +static esp_err_t esp_rmaker_ota_handle_metadata_common(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) +{ + /* Handle OTA metadata, if any */ + if (ota_data->metadata) { + esp_rmaker_ota_action_t metadata_result = esp_rmaker_ota_handle_metadata(ota_handle, ota_data); + if (metadata_result != OTA_OK) { + ESP_LOGW(TAG, "Cannot proceed with the OTA as per the metadata received."); + return ESP_FAIL; + } + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_ota_success_reboot_sequence(esp_rmaker_ota_handle_t ota_handle, const char *protocol_name, int attempt_count) +{ + /* Success path: rest of the reboot/rollback logic */ +#ifdef CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE + nvs_handle handle; + esp_err_t nvs_err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (nvs_err == ESP_OK) { + uint8_t ota_update = 1; + nvs_set_blob(handle, RMAKER_OTA_UPDATE_FLAG_NVS_NAME, &ota_update, sizeof(ota_update)); + nvs_close(handle); + } + char reboot_info[80]; + snprintf(reboot_info, sizeof(reboot_info), "Rebooting into new firmware (after %d %s attempt%s)", + attempt_count, protocol_name, (attempt_count == 1) ? "" : "s"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, reboot_info); +#else + char success_info[80]; + snprintf(success_info, sizeof(success_info), "%s OTA Upgrade finished successfully (after %d attempt%s)", + protocol_name, attempt_count, (attempt_count == 1) ? "" : "s"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_SUCCESS, success_info); +#endif +#ifndef CONFIG_ESP_RMAKER_OTA_DISABLE_AUTO_REBOOT + ESP_LOGI(TAG, "%s OTA upgrade successful. Rebooting in %d seconds...", protocol_name, OTA_REBOOT_TIMER_SEC); + esp_rmaker_reboot(OTA_REBOOT_TIMER_SEC); +#else + ESP_LOGI(TAG, "%s OTA upgrade successful. Auto reboot is disabled. Requesting a Reboot via Event handler.", protocol_name); + esp_rmaker_ota_post_event(RMAKER_OTA_EVENT_REQ_FOR_REBOOT, NULL, 0); +#endif + return ESP_OK; +} + /* Local macro for OTA max retries, can be overridden for development */ #ifndef ESP_RMAKER_OTA_MAX_RETRIES #define ESP_RMAKER_OTA_MAX_RETRIES CONFIG_ESP_RMAKER_OTA_MAX_RETRIES @@ -346,249 +467,18 @@ esp_rmaker_ota_action_t esp_rmaker_ota_handle_metadata(esp_rmaker_ota_handle_t o #define ESP_RMAKER_OTA_RETRY_DELAY_SECONDS (CONFIG_ESP_RMAKER_OTA_RETRY_DELAY_MINUTES * 60) #endif -/* Helper function to perform OTA download and validation, returns ESP_OK on success, ESP_FAIL on failure. */ -static esp_err_t esp_rmaker_ota_perform_with_validation(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data, char *err_desc, size_t err_desc_size) -{ - int buffer_size_tx = DEF_HTTP_TX_BUFFER_SIZE; - /* In case received url is longer, we will increase the tx buffer size - * to accomodate the longer url and other headers. - */ - if (strlen(ota_data->url) > buffer_size_tx) { - buffer_size_tx = strlen(ota_data->url) + 128; - } - - if (ota_data->filesize) { - ESP_LOGD(TAG, "Received file size: %d", ota_data->filesize); - } - - esp_err_t ota_finish_err = ESP_OK; - esp_http_client_config_t config = { - .url = ota_data->url, -#ifdef ESP_RMAKER_USE_CERT_BUNDLE - .crt_bundle_attach = esp_crt_bundle_attach, -#else - .cert_pem = ota_data->server_cert, -#endif - .timeout_ms = 5000, - .buffer_size = DEF_HTTP_RX_BUFFER_SIZE, - .buffer_size_tx = buffer_size_tx, - .keep_alive_enable = true - }; -#ifdef CONFIG_ESP_RMAKER_SKIP_COMMON_NAME_CHECK - config.skip_cert_common_name_check = true; -#endif - - esp_https_ota_config_t ota_config = { - .http_config = &config, - }; - /* Using a warning just to highlight the message */ - ESP_LOGW(TAG, "Starting OTA. This may take time."); - esp_https_ota_handle_t https_ota_handle = NULL; - esp_err_t err = esp_https_ota_begin(&ota_config, &https_ota_handle); - if (err != ESP_OK) { - int err_no = errno; - snprintf(err_desc, err_desc_size, "OTA Begin failed: %s (errno=%d: %s)", esp_err_to_name(err), err_no, err_no ? strerror(err_no) : "Invalid"); - return ESP_FAIL; - } - -#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI -/* Get the current Wi-Fi power save type. In case OTA fails and we need this - * to restore power saving. - */ - wifi_ps_type_t ps_type; - esp_wifi_get_ps(&ps_type); -/* Disable Wi-Fi power save to speed up OTA, iff BT is controller is idle/disabled. - * Co-ex requirement, device panics otherwise.*/ -#if CONFIG_BT_ENABLED - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { - esp_wifi_set_ps(WIFI_PS_NONE); - } -#else - esp_wifi_set_ps(WIFI_PS_NONE); -#endif /* CONFIG_BT_ENABLED */ -#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ - - esp_app_desc_t app_desc; - err = esp_https_ota_get_img_desc(https_ota_handle, &app_desc); - if (err != ESP_OK) { - int err_no = errno; - snprintf(err_desc, err_desc_size, "Failed to read image description: %s (errno=%d: %s)", esp_err_to_name(err), err_no, err_no ? strerror(err_no) : "Invalid"); - /* OTA failed, may retry later */ - goto ota_end; - } - err = validate_image_header(ota_handle, &app_desc); - if (err != ESP_OK) { - snprintf(err_desc, err_desc_size, "Image header verification failed"); - /* OTA should be rejected, returning ESP_ERR_INVALID_STATE */ - err = ESP_ERR_INVALID_STATE; - goto ota_end; - } - - /* Report status: Downloading Firmware Image */ - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Downloading Firmware Image"); - - int count = 0; -#ifdef CONFIG_ESP_RMAKER_OTA_PROGRESS_SUPPORT - last_ota_progress = 0; -#endif - while (1) { - err = esp_https_ota_perform(https_ota_handle); - if (err == ESP_ERR_INVALID_VERSION) { - snprintf(err_desc, err_desc_size, "Chip revision mismatch"); - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_REJECTED, err_desc); - /* OTA should be rejected, returning ESP_ERR_INVALID_STATE */ - err = ESP_ERR_INVALID_STATE; - goto ota_end; - } - if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) { - break; - } - /* esp_https_ota_perform returns after every read operation which gives user the ability to - * monitor the status of OTA upgrade by calling esp_https_ota_get_image_len_read, which gives length of image - * data read so far. - * We are using a counter just to reduce the number of prints - */ - count++; - if (count == 50) { - ESP_LOGI(TAG, "Image bytes read: %d", esp_https_ota_get_image_len_read(https_ota_handle)); - count = 0; - } -#ifdef CONFIG_ESP_RMAKER_OTA_PROGRESS_SUPPORT - int image_size = esp_https_ota_get_image_size(https_ota_handle); - int read_size = esp_https_ota_get_image_len_read(https_ota_handle); - int ota_progress = 100 * read_size / image_size; // The unit is % - /* When ota_progress is 0 or 100, we will not report the progress, beacasue the 0 and 100 is reported by additional_info `Downloading Firmware Image` and - * `Firmware Image download complete`. And every progress will only report once and the progress is increasing. - */ - if (((ota_progress != 0) && (ota_progress != 100)) && (ota_progress % CONFIG_ESP_RMAKER_OTA_PROGRESS_INTERVAL == 0) && (last_ota_progress < ota_progress)) { - last_ota_progress = ota_progress; - char description[40] = {0}; - snprintf(description, sizeof(description), "Downloaded %d%% Firmware Image", ota_progress); - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, description); - } -#endif - } - if (err != ESP_OK) { - int err_no = errno; - snprintf(err_desc, err_desc_size, "OTA failed: %s (errno=%d: %s)", esp_err_to_name(err), err_no, err_no ? strerror(err_no) : "Invalid"); - /* OTA failed, may retry later */ - goto ota_end; - } - - if (esp_https_ota_is_complete_data_received(https_ota_handle) != true) { - snprintf(err_desc, err_desc_size, "Complete data was not received"); - /* OTA failed, may retry later */ - err = ESP_FAIL; - goto ota_end; - } - - /* Report completion before finishing */ - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, "Firmware Image download complete"); - -ota_end: -#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI -#ifdef CONFIG_BT_ENABLED - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { - esp_wifi_set_ps(ps_type); - } -#else - esp_wifi_set_ps(ps_type); -#endif /* CONFIG_BT_ENABLED */ -#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ - - if (err == ESP_OK) { - /* Success path: finish the OTA */ - ota_finish_err = esp_https_ota_finish(https_ota_handle); - if (ota_finish_err == ESP_OK) { - return ESP_OK; - } else if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { - snprintf(err_desc, err_desc_size, "Image validation failed"); - } else { - int err_no = errno; - snprintf(err_desc, err_desc_size, "OTA finish failed: %s (errno=%d: %s)", esp_err_to_name(ota_finish_err), err_no, err_no ? strerror(err_no) : "Invalid"); - } - /* Handle already closed by esp_https_ota_finish(), don't call abort */ - return ESP_FAIL; - } - - /* Error path: abort the OTA */ - esp_https_ota_abort(https_ota_handle); - return (err == ESP_ERR_INVALID_STATE) ? ESP_ERR_INVALID_STATE : ESP_FAIL; -} esp_err_t esp_rmaker_ota_default_cb(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data) { - if (!ota_data->url) { - return ESP_FAIL; - } - /* Handle OTA metadata, if any */ - if (ota_data->metadata) { - if (esp_rmaker_ota_handle_metadata(ota_handle, ota_data) != OTA_OK) { - ESP_LOGW(TAG, "Cannot proceed with the OTA as per the metadata received."); - return ESP_FAIL; - } - } - esp_rmaker_ota_post_event(RMAKER_OTA_EVENT_STARTING, NULL, 0); - - esp_err_t err = ESP_FAIL; - char err_desc[128] = {0}; - int attempt; - for (attempt = 0; attempt < ESP_RMAKER_OTA_MAX_RETRIES; ++attempt) { - char info[64]; - snprintf(info, sizeof(info), "Starting OTA Upgrade (attempt %d/%d)", attempt + 1, ESP_RMAKER_OTA_MAX_RETRIES); - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, info); - ESP_LOGW(TAG, "Starting OTA attempt %d/%d. This may take time.", attempt + 1, ESP_RMAKER_OTA_MAX_RETRIES); - err = esp_rmaker_ota_perform_with_validation(ota_handle, ota_data, err_desc, sizeof(err_desc)); - if (err == ESP_OK) { - break; - } else if (err == ESP_ERR_INVALID_STATE) { - return ESP_FAIL; - } else { - ESP_LOGE(TAG, "OTA attempt %d failed: %s", attempt + 1, err_desc); - /* Report failure for this attempt */ - char fail_info[192]; - snprintf(fail_info, sizeof(fail_info), "Attempt %d/%d failed: %s", attempt + 1, ESP_RMAKER_OTA_MAX_RETRIES, err_desc); - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, fail_info); - } - } - if (err != ESP_OK) { - /* Final failure already reported in the loop above */ - /* If OTA is using topics, schedule a re-fetch after the configured delay */ - esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)ota_handle; - if (ota->type == OTA_USING_TOPICS) { - esp_rmaker_ota_fetch_with_delay(ESP_RMAKER_OTA_RETRY_DELAY_SECONDS); - } - return ESP_FAIL; - } - /* Success path: rest of the reboot/rollback logic as before */ -#ifdef CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE - nvs_handle handle; - esp_err_t nvs_err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, RMAKER_OTA_NVS_NAMESPACE, NVS_READWRITE, &handle); - if (nvs_err == ESP_OK) { - uint8_t ota_update = 1; - nvs_set_blob(handle, RMAKER_OTA_UPDATE_FLAG_NVS_NAME, &ota_update, sizeof(ota_update)); - nvs_close(handle); - } - char reboot_info[80]; - snprintf(reboot_info, sizeof(reboot_info), "Rebooting into new firmware (after %d attempt%s)", attempt + 1, (attempt == 0) ? "" : "s"); - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_IN_PROGRESS, reboot_info); -#else - char success_info[80]; - snprintf(success_info, sizeof(success_info), "OTA Upgrade finished successfully (after %d attempt%s)", attempt + 1, (attempt == 0) ? "" : "s"); - esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_SUCCESS, success_info); +#ifdef CONFIG_ESP_RMAKER_OTA_USE_HTTPS + return esp_rmaker_ota_https_cb(ota_handle, ota_data); +#else /* CONFIG_ESP_RMAKER_OTA_USE_MQTT */ + return esp_rmaker_ota_mqtt_cb(ota_handle, ota_data); #endif -#ifndef CONFIG_ESP_RMAKER_OTA_DISABLE_AUTO_REBOOT - ESP_LOGI(TAG, "OTA upgrade successful. Rebooting in %d seconds...", OTA_REBOOT_TIMER_SEC); - esp_rmaker_reboot(OTA_REBOOT_TIMER_SEC); -#else - 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; } -static void event_handler(void* arg, esp_event_base_t event_base, - int32_t event_id, void* event_data) +static void event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { esp_rmaker_ota_t *ota = (esp_rmaker_ota_t *)arg; esp_rmaker_ota_diag_status_t diag_status = OTA_DIAG_STATUS_SUCCESS; @@ -752,9 +642,12 @@ static void esp_rmaker_ota_manage_rollback(esp_rmaker_ota_t *ota) } } +/* Protocol-agnostic default config - no protocol-specific dependencies */ static const esp_rmaker_ota_config_t ota_default_config = { - .server_cert = esp_rmaker_ota_def_cert, + .server_cert = NULL, /* Each protocol handles its own certificate defaults */ + .priv = NULL, }; + /* Enable the ESP RainMaker specific OTA */ esp_err_t esp_rmaker_ota_enable(esp_rmaker_ota_config_t *ota_config, esp_rmaker_ota_type_t type) { 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 218cba7..83b0dd2 100644 --- a/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h +++ b/components/esp_rainmaker/src/ota/esp_rmaker_ota_internal.h @@ -1,24 +1,23 @@ -// 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. +/* + * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ #pragma once + #include #include #include #include #include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif #define RMAKER_OTA_NVS_NAMESPACE "rmaker_ota" #define RMAKER_OTA_JOB_ID_NVS_NAME "rmaker_ota_id" @@ -34,6 +33,9 @@ typedef struct { const char *server_cert; char *url; char *fw_version; +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT + char *stream_id; +#endif int filesize; bool ota_in_progress; bool validation_in_progress; @@ -43,7 +45,16 @@ typedef struct { char *metadata; } esp_rmaker_ota_t; + + char *esp_rmaker_ota_status_to_string(ota_status_t status); +esp_err_t esp_rmaker_ota_post_event(esp_rmaker_event_t event_id, void *data, size_t data_size); +typedef enum { + OTA_OK = 0, + OTA_ERR, + OTA_DELAYED +} esp_rmaker_ota_action_t; + 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); @@ -53,3 +64,17 @@ esp_err_t esp_rmaker_ota_report_status_using_params(esp_rmaker_ota_handle_t ota_ 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, ota_status_t status, char *additional_info); + +/* Common retry loop infrastructure with function pointer pattern */ +typedef esp_err_t (*ota_protocol_func_t)(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data, char *err_desc, size_t err_desc_size); + +/* Complete OTA workflow orchestration */ +esp_err_t esp_rmaker_ota_start_workflow(esp_rmaker_ota_handle_t ota_handle, esp_rmaker_ota_data_t *ota_data, + ota_protocol_func_t protocol_func, const char *protocol_name); + +/* OTA image header validation */ +esp_err_t validate_image_header(esp_rmaker_ota_handle_t ota_handle, esp_app_desc_t *new_app_info); + +#ifdef __cplusplus +} +#endif 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 3c2cece..f6ae3be 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 @@ -1,16 +1,8 @@ -// 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. +/* + * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ #include #include @@ -93,7 +85,7 @@ esp_err_t esp_rmaker_ota_report_status_using_params(esp_rmaker_ota_handle_t ota_ esp_rmaker_param_update_and_report(info_param, esp_rmaker_str(additional_info)); esp_rmaker_param_update_and_report(status_param, esp_rmaker_str(esp_rmaker_ota_status_to_string(status))); - + return ESP_OK; } 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 5bff38c..42bb441 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 @@ -1,16 +1,9 @@ -// 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. +/* + * SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + #include #include #include @@ -106,8 +99,8 @@ static void ota_fetch_timeout_timer_cb(TimerHandle_t xTimer) -static void ota_fetch_mqtt_event_handler(void* arg, esp_event_base_t event_base, - int32_t event_id, void* event_data) +static void ota_fetch_mqtt_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { if (!event_data || event_base != RMAKER_COMMON_EVENT) { return; @@ -183,6 +176,12 @@ void esp_rmaker_ota_finish_using_topics(esp_rmaker_ota_t *ota) free(ota->url); ota->url = NULL; } +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT + if (ota->stream_id) { + free(ota->stream_id); + ota->stream_id = NULL; + } +#endif ota->filesize = 0; if (ota->transient_priv) { free(ota->transient_priv); @@ -222,6 +221,9 @@ static void ota_url_handler(const char *topic, void *payload, size_t payload_len */ jparse_ctx_t jctx; char *url = NULL, *ota_job_id = NULL, *fw_version = NULL; +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT + char *stream_id = NULL; +#endif int ret = json_parse_start(&jctx, (char *)payload, (int) payload_len); if (ret != 0) { ESP_LOGE(TAG, "Invalid JSON received: %s", (char *)payload); @@ -269,10 +271,27 @@ static void ota_url_handler(const char *topic, void *payload, size_t payload_len json_obj_get_string(&jctx, "url", url, len); ESP_LOGI(TAG, "URL: %s", url); +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT + len = 0; + ret = json_obj_get_strlen(&jctx, "stream_id", &len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Aborted. Stream ID not found in JSON"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. Stream ID not found in JSON"); + goto end; + } + len++; /* Increment for NULL character */ + stream_id = calloc(1, len); + if (!stream_id) { + ESP_LOGE(TAG, "Aborted. Stream ID memory allocation failed"); + esp_rmaker_ota_report_status(ota_handle, OTA_STATUS_FAILED, "Aborted. Stream ID memory allocation failed"); + goto end; + } + json_obj_get_string(&jctx, "stream_id", stream_id, len); + ESP_LOGI(TAG, "Stream ID: %s", stream_id); +#endif int filesize = 0; json_obj_get_int(&jctx, "file_size", &filesize); ESP_LOGI(TAG, "File Size: %d", filesize); - len = 0; ret = json_obj_get_strlen(&jctx, "fw_version", &len); if (ret == ESP_OK && len > 0) { @@ -307,6 +326,9 @@ static void ota_url_handler(const char *topic, void *payload, size_t payload_len free(ota->url); } ota->url = url; +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT + ota->stream_id = stream_id; +#endif ota->fw_version = fw_version; ota->filesize = filesize; ota->ota_in_progress = true; @@ -318,6 +340,11 @@ end: if (url) { free(url); } +#ifdef CONFIG_ESP_RMAKER_OTA_USE_MQTT + if (stream_id) { + free(stream_id); + } +#endif if (fw_version) { free(fw_version); } @@ -543,4 +570,4 @@ esp_err_t esp_rmaker_ota_fetch_with_delay(int time) xTimerStart(timer, 0); } return ESP_OK; -} \ No newline at end of file +}