Added bootloader_common_get_sha256_of_partition() and esp_partition_get_sha256() - get or calculate SHA-256
digest for app and data partitions.
Added bootloader_sha256_hex_to_str() - helps to print SHA-256 digest
Added esp_partition_check_identity() - compares two partitions by SHA-256 digest
Refactoring a function esp_image_load() in bootloader space to esp_image_verify() and
bootloader_load_image(). Old name function esp_image_load is deprecated
and will remove in V4.0 version.
spi_flash/sim: Fix error test_host. Add stub for bootloader_common_get_sha256_of_partition in sim/stubs
.size = it->part->size,
};
- if (esp_image_load(ESP_IMAGE_VERIFY, &part_pos, &data) != ESP_OK) {
+ if (esp_image_verify(ESP_IMAGE_VERIFY, &part_pos, &data) != ESP_OK) {
ret = ESP_ERR_OTA_VALIDATE_FAILED;
goto cleanup;
}
.offset = partition->address,
.size = partition->size,
};
- if (esp_image_load(ESP_IMAGE_VERIFY, &part_pos, &data) != ESP_OK) {
+ if (esp_image_verify(ESP_IMAGE_VERIFY, &part_pos, &data) != ESP_OK) {
return ESP_ERR_OTA_VALIDATE_FAILED;
}
* If the OTA data partition is not present or not valid then the result is the first app partition found in the
* partition table. In priority order, this means: the factory app, the first OTA app slot, or the test app partition.
*
- * Note that there is no guarantee the returned partition is a valid app. Use esp_image_load(ESP_IMAGE_VERIFY, ...) to verify if the
+ * Note that there is no guarantee the returned partition is a valid app. Use esp_image_verify(ESP_IMAGE_VERIFY, ...) to verify if the
* returned partition contains a bootable image.
*
* @return Pointer to info for partition structure, or NULL if partition table is invalid or a flash read operation failed. Any returned pointer is valid for the lifetime of the application.
* @return Returns true if the list contains the label, false otherwise.
*/
bool bootloader_common_label_search(const char *list, char *label);
+
+/**
+ * @brief Calculates a sha-256 for a given partition or returns a appended digest.
+ *
+ * This function can be used to return the SHA-256 digest of application, bootloader and data partitions.
+ * For apps with SHA-256 appended to the app image, the result is the appended SHA-256 value for the app image content.
+ * The hash is verified before returning, if app content is invalid then the function returns ESP_ERR_IMAGE_INVALID.
+ * For apps without SHA-256 appended to the image, the result is the SHA-256 of all bytes in the app image.
+ * For other partition types, the result is the SHA-256 of the entire partition.
+ *
+ * @param[in] address Address of partition.
+ * @param[in] size Size of partition.
+ * @param[in] type Type of partition. For applications the type is 0, otherwise type is data.
+ * @param[out] out_sha_256 Returned SHA-256 digest for a given partition.
+ *
+ * @return
+ * - ESP_OK: In case of successful operation.
+ * - ESP_ERR_INVALID_ARG: The size was 0 or the sha_256 was NULL.
+ * - ESP_ERR_NO_MEM: Cannot allocate memory for sha256 operation.
+ * - ESP_ERR_IMAGE_INVALID: App partition doesn't contain a valid app image.
+ * - ESP_FAIL: An allocation error occurred.
+ */
+esp_err_t bootloader_common_get_sha256_of_partition(uint32_t address, uint32_t size, int type, uint8_t *out_sha_256);
esp_image_segment_header_t segments[ESP_IMAGE_MAX_SEGMENTS]; /* Per-segment header data */
uint32_t segment_data[ESP_IMAGE_MAX_SEGMENTS]; /* Data offsets for each segment */
uint32_t image_len; /* Length of image on flash, in bytes */
+ uint8_t image_digest[32]; /* appended SHA-256 digest */
} esp_image_metadata_t;
/* Mode selection for esp_image_load() */
typedef enum {
- ESP_IMAGE_VERIFY, /* Verify image contents, load metadata. Print errorsors. */
+ ESP_IMAGE_VERIFY, /* Verify image contents, load metadata. Print errors. */
ESP_IMAGE_VERIFY_SILENT, /* Verify image contents, load metadata. Don't print errors. */
#ifdef BOOTLOADER_BUILD
ESP_IMAGE_LOAD, /* Verify image contents, load to memory. Print errors. */
/**
* @brief Verify and (optionally, in bootloader mode) load an app image.
*
+ * This name is deprecated and is included for compatibility with the ESP-IDF v3.x API.
+ * It will be removed in V4.0 version.
+ * Function has been renamed to esp_image_verify().
+ * Use function esp_image_verify() to verify a image. And use function bootloader_load_image() to load image from a bootloader space.
+ *
* If encryption is enabled, data will be transparently decrypted.
*
* @param mode Mode of operation (verify, silent verify, or load).
* - ESP_ERR_IMAGE_INVALID if the image appears invalid.
* - ESP_ERR_INVALID_ARG if the partition or data pointers are invalid.
*/
-esp_err_t esp_image_load(esp_image_load_mode_t mode, const esp_partition_pos_t *part, esp_image_metadata_t *data);
+esp_err_t esp_image_load(esp_image_load_mode_t mode, const esp_partition_pos_t *part, esp_image_metadata_t *data) __attribute__((deprecated));
+
+/**
+ * @brief Verify an app image.
+ *
+ * If encryption is enabled, data will be transparently decrypted.
+ *
+ * @param mode Mode of operation (verify, silent verify, or load).
+ * @param part Partition to load the app from.
+ * @param[inout] data Pointer to the image metadata structure which is be filled in by this function.
+ * 'start_addr' member should be set (to the start address of the image.)
+ * Other fields will all be initialised by this function.
+ *
+ * Image validation checks:
+ * - Magic byte.
+ * - Partition smaller than 16MB.
+ * - All segments & image fit in partition.
+ * - 8 bit image checksum is valid.
+ * - SHA-256 of image is valid (if image has this appended).
+ * - (Signature) if signature verification is enabled.
+ *
+ * @return
+ * - ESP_OK if verify or load was successful
+ * - ESP_ERR_IMAGE_FLASH_FAIL if a SPI flash error occurs
+ * - ESP_ERR_IMAGE_INVALID if the image appears invalid.
+ * - ESP_ERR_INVALID_ARG if the partition or data pointers are invalid.
+ */
+esp_err_t esp_image_verify(esp_image_load_mode_t mode, const esp_partition_pos_t *part, esp_image_metadata_t *data);
+
+/**
+ * @brief Verify and load an app image (available only in space of bootloader).
+ *
+ * If encryption is enabled, data will be transparently decrypted.
+ *
+ * @param part Partition to load the app from.
+ * @param[inout] data Pointer to the image metadata structure which is be filled in by this function.
+ * 'start_addr' member should be set (to the start address of the image.)
+ * Other fields will all be initialised by this function.
+ *
+ * Image validation checks:
+ * - Magic byte.
+ * - Partition smaller than 16MB.
+ * - All segments & image fit in partition.
+ * - 8 bit image checksum is valid.
+ * - SHA-256 of image is valid (if image has this appended).
+ * - (Signature) if signature verification is enabled.
+ *
+ * @return
+ * - ESP_OK if verify or load was successful
+ * - ESP_ERR_IMAGE_FLASH_FAIL if a SPI flash error occurs
+ * - ESP_ERR_IMAGE_INVALID if the image appears invalid.
+ * - ESP_ERR_INVALID_ARG if the partition or data pointers are invalid.
+ */
+esp_err_t bootloader_load_image(const esp_partition_pos_t *part, esp_image_metadata_t *data);
/**
* @brief Verify the bootloader image.
#include <stdint.h>
#include <stdlib.h>
+#include "esp_err.h"
typedef void *bootloader_sha256_handle_t;
void bootloader_sha256_data(bootloader_sha256_handle_t handle, const void *data, size_t data_len);
void bootloader_sha256_finish(bootloader_sha256_handle_t handle, uint8_t *digest);
+
+/**
+ * @brief Converts an array to a printable string.
+ *
+ * This function is useful for printing SHA-256 digest.
+ * \code{c}
+ * // Example of using. image_hash will be printed
+ * #define HASH_LEN 32 // SHA-256 digest length
+ * ...
+ * char hash_print[HASH_LEN * 2 + 1];
+ * hash_print[HASH_LEN * 2] = 0;
+ * bootloader_sha256_hex_to_str(hash_print, image_hash, HASH_LEN);
+ * ESP_LOGI(TAG, %s", hash_print);
+ * \endcode
+
+ * @param[out] out_str Output string
+ * @param[in] in_array_hex Pointer to input array
+ * @param[in] len Length of input array
+ *
+ * @return ESP_OK: Successful
+ * ESP_ERR_INVALID_ARG: Error in the passed arguments
+ */
+esp_err_t bootloader_sha256_hex_to_str(char *out_str, const uint8_t *in_array_hex, size_t len);
#include "bootloader_flash.h"
#include "bootloader_common.h"
#include "soc/gpio_periph.h"
+#include "esp_image_format.h"
+#include "bootloader_sha.h"
+
+#define ESP_PARTITION_HASH_LEN 32 /* SHA-256 digest length */
static const char* TAG = "boot_comm";
return ret;
}
+
+esp_err_t bootloader_common_get_sha256_of_partition (uint32_t address, uint32_t size, int type, uint8_t *out_sha_256)
+{
+ if (out_sha_256 == NULL || size == 0) {
+ return ESP_ERR_INVALID_ARG;
+ }
+
+ if (type == PART_TYPE_APP) {
+ const esp_partition_pos_t partition_pos = {
+ .offset = address,
+ .size = size,
+ };
+ esp_image_metadata_t data;
+ // Function esp_image_verify() verifies and fills the structure data.
+ // here important to get: image_digest, image_len, hash_appended.
+ if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &partition_pos, &data) != ESP_OK) {
+ return ESP_ERR_IMAGE_INVALID;
+ }
+ if (data.image.hash_appended) {
+ memcpy(out_sha_256, data.image_digest, ESP_PARTITION_HASH_LEN);
+ return ESP_OK;
+ }
+ // If image doesn't have a appended hash then hash calculates for entire image.
+ size = data.image_len;
+ }
+ // If image is type by data then hash is calculated for entire image.
+ const void *partition_bin = bootloader_mmap(address, size);
+ if (partition_bin == NULL) {
+ ESP_LOGE(TAG, "bootloader_mmap(0x%x, 0x%x) failed", address, size);
+ return ESP_FAIL;
+ }
+ bootloader_sha256_handle_t sha_handle = bootloader_sha256_start();
+ if (sha_handle == NULL) {
+ bootloader_munmap(partition_bin);
+ return ESP_ERR_NO_MEM;
+ }
+ bootloader_sha256_data(sha_handle, partition_bin, size);
+ bootloader_sha256_finish(sha_handle, out_sha_256);
+
+ bootloader_munmap(partition_bin);
+
+ return ESP_OK;
+}
}
#endif
+
+esp_err_t bootloader_sha256_hex_to_str(char *out_str, const uint8_t *in_array_hex, size_t len)
+{
+ if (out_str == NULL || in_array_hex == NULL || len == 0) {
+ return ESP_ERR_INVALID_ARG;
+ }
+ for (int i = 0; i < len; i++) {
+ for (int shift = 0; shift < 2; shift++) {
+ uint8_t nibble = (in_array_hex[i] >> (shift ? 0 : 4)) & 0x0F;
+ if (nibble < 10) {
+ out_str[i*2+shift] = '0' + nibble;
+ } else {
+ out_str[i*2+shift] = 'a' + nibble - 10;
+ }
+ }
+ }
+ return ESP_OK;
+}
#include "bootloader_config.h"
#include "bootloader_common.h"
#include "bootloader_utility.h"
+#include "bootloader_sha.h"
static const char* TAG = "boot";
return false;
}
#ifdef BOOTLOADER_BUILD
- if (esp_image_load(ESP_IMAGE_LOAD, partition, data) == ESP_OK) {
+ if (bootloader_load_image(partition, data) == ESP_OK) {
ESP_LOGI(TAG, "Loaded app from partition at offset 0x%x",
partition->offset);
return true;
static esp_err_t __attribute__((unused)) verify_secure_boot_signature(bootloader_sha256_handle_t sha_handle, esp_image_metadata_t *data);
static esp_err_t __attribute__((unused)) verify_simple_hash(bootloader_sha256_handle_t sha_handle, esp_image_metadata_t *data);
-esp_err_t esp_image_load(esp_image_load_mode_t mode, const esp_partition_pos_t *part, esp_image_metadata_t *data)
+static esp_err_t image_load(esp_image_load_mode_t mode, const esp_partition_pos_t *part, esp_image_metadata_t *data)
{
#ifdef BOOTLOADER_BUILD
bool do_load = (mode == ESP_IMAGE_LOAD);
err = verify_image_header(data->start_addr, &data->image, silent);
if (err != ESP_OK) {
-goto err;
+ goto err;
}
if (data->image.segment_count > ESP_IMAGE_MAX_SEGMENTS) {
bootloader_sha256_finish(sha_handle, NULL);
}
}
+
+ if (data->image.hash_appended) {
+ const void *hash = bootloader_mmap(data->start_addr + data->image_len - HASH_LEN, HASH_LEN);
+ if (hash == NULL) {
+ err = ESP_FAIL;
+ goto err;
+ }
+ memcpy(data->image_digest, hash, HASH_LEN);
+ bootloader_munmap(hash);
+ }
+
sha_handle = NULL;
if (err != ESP_OK) {
goto err;
return err;
}
+esp_err_t bootloader_load_image(const esp_partition_pos_t *part, esp_image_metadata_t *data)
+{
+#ifdef BOOTLOADER_BUILD
+ return image_load(ESP_IMAGE_LOAD, part, data);
+#else
+ return ESP_FAIL;
+#endif
+}
+
+esp_err_t esp_image_verify(esp_image_load_mode_t mode, const esp_partition_pos_t *part, esp_image_metadata_t *data)
+{
+ return image_load(mode, part, data);
+}
+
+esp_err_t esp_image_load(esp_image_load_mode_t mode, const esp_partition_pos_t *part, esp_image_metadata_t *data) __attribute__((alias("esp_image_verify")));
+
static esp_err_t verify_image_header(uint32_t src_addr, const esp_image_header_t *image, bool silent)
{
esp_err_t err = ESP_OK;
.offset = ESP_BOOTLOADER_OFFSET,
.size = ESP_PARTITION_TABLE_OFFSET - ESP_BOOTLOADER_OFFSET,
};
- esp_err_t err = esp_image_load(ESP_IMAGE_VERIFY,
+ esp_err_t err = esp_image_verify(ESP_IMAGE_VERIFY,
&bootloader_part,
&data);
if (length != NULL) {
static void debug_log_hash(const uint8_t *image_hash, const char *label)
{
#if BOOT_LOG_LEVEL >= LOG_LEVEL_DEBUG
- char hash_print[sizeof(image_hash)*2 + 1];
- hash_print[sizeof(image_hash)*2] = 0;
- for (int i = 0; i < sizeof(image_hash); i++) {
- for (int shift = 0; shift < 2; shift++) {
- uint8_t nibble = (image_hash[i] >> (shift ? 0 : 4)) & 0x0F;
- if (nibble < 10) {
- hash_print[i*2+shift] = '0' + nibble;
- } else {
- hash_print[i*2+shift] = 'a' + nibble - 10;
- }
- }
- }
- ESP_LOGD(TAG, "%s: %s", label, hash_print);
+ char hash_print[HASH_LEN * 2 + 1];
+ hash_print[HASH_LEN * 2] = 0;
+ bootloader_sha256_hex_to_str(hash_print, image_hash, HASH_LEN);
+ ESP_LOGD(TAG, "%s: %s", label, hash_print);
#endif
}
if (partition->type == PART_TYPE_APP) {
/* check if the partition holds a valid unencrypted app */
esp_image_metadata_t data_ignored;
- err = esp_image_load(ESP_IMAGE_VERIFY,
+ err = esp_image_verify(ESP_IMAGE_VERIFY,
&partition->pos,
&data_ignored);
should_encrypt = (err == ESP_OK);
.size = ESP_PARTITION_TABLE_OFFSET - ESP_BOOTLOADER_OFFSET,
};
esp_image_metadata_t data = { 0 };
- TEST_ASSERT_EQUAL_HEX(ESP_OK, esp_image_load(ESP_IMAGE_VERIFY, &fake_bootloader_partition, &data));
+ TEST_ASSERT_EQUAL_HEX(ESP_OK, esp_image_verify(ESP_IMAGE_VERIFY, &fake_bootloader_partition, &data));
TEST_ASSERT_NOT_EQUAL(0, data.image_len);
uint32_t bootloader_length = 0;
.size = running->size,
};
- TEST_ASSERT_EQUAL_HEX(ESP_OK, esp_image_load(ESP_IMAGE_VERIFY, &running_pos, &data));
+ TEST_ASSERT_EQUAL_HEX(ESP_OK, esp_image_verify(ESP_IMAGE_VERIFY, &running_pos, &data));
TEST_ASSERT_NOT_EQUAL(0, data.image_len);
TEST_ASSERT_TRUE(data.image_len <= running->size);
}
spi_flash_mmap_memory_t memory,
const void** out_ptr, spi_flash_mmap_handle_t* out_handle);
+/**
+ * @brief Get SHA-256 digest for required partition.
+ *
+ * For apps with SHA-256 appended to the app image, the result is the appended SHA-256 value for the app image content.
+ * The hash is verified before returning, if app content is invalid then the function returns ESP_ERR_IMAGE_INVALID.
+ * For apps without SHA-256 appended to the image, the result is the SHA-256 of all bytes in the app image.
+ * For other partition types, the result is the SHA-256 of the entire partition.
+ *
+ * @param[in] partition Pointer to info for partition containing app or data. (fields: address, size and type, are required to be filled).
+ * @param[out] sha_256 Returned SHA-256 digest for a given partition.
+ *
+ * @return
+ * - ESP_OK: In case of successful operation.
+ * - ESP_ERR_INVALID_ARG: The size was 0 or the sha_256 was NULL.
+ * - ESP_ERR_NO_MEM: Cannot allocate memory for sha256 operation.
+ * - ESP_ERR_IMAGE_INVALID: App partition doesn't contain a valid app image.
+ * - ESP_FAIL: An allocation error occurred.
+ */
+esp_err_t esp_partition_get_sha256(const esp_partition_t *partition, uint8_t *sha_256);
+
+/**
+ * @brief Check for the identity of two partitions by SHA-256 digest.
+ *
+ * @param[in] partition_1 Pointer to info for partition 1 containing app or data. (fields: address, size and type, are required to be filled).
+ * @param[in] partition_2 Pointer to info for partition 2 containing app or data. (fields: address, size and type, are required to be filled).
+ *
+ * @return
+ * - True: In case of the two firmware is equal.
+ * - False: Otherwise
+ */
+bool esp_partition_check_identity(const esp_partition_t *partition_1, const esp_partition_t *partition_2);
#ifdef __cplusplus
}
#include "esp_partition.h"
#include "esp_flash_encrypt.h"
#include "esp_log.h"
+#include "bootloader_common.h"
+#define HASH_LEN 32 /* SHA-256 digest length */
#ifndef NDEBUG
// Enable built-in checks in queue.h in debug builds
}
return rc;
}
+
+esp_err_t esp_partition_get_sha256(const esp_partition_t *partition, uint8_t *sha_256)
+{
+ return bootloader_common_get_sha256_of_partition(partition->address, partition->size, partition->type, sha_256);
+}
+
+bool esp_partition_check_identity(const esp_partition_t *partition_1, const esp_partition_t *partition_2)
+{
+ uint8_t sha_256[2][HASH_LEN] = { 0 };
+
+ if (esp_partition_get_sha256(partition_1, sha_256[0]) == ESP_OK &&
+ esp_partition_get_sha256(partition_2, sha_256[1]) == ESP_OK) {
+
+ if (memcmp(sha_256[0], sha_256[1], HASH_LEN) == 0) {
+ // The partitions are identity
+ return true;
+ }
+ }
+ return false;
+}
log/log.c \
newlib/lock.c \
esp32/crc.cpp \
- esp32/esp_random.c
+ esp32/esp_random.c \
+ bootloader_support/src/bootloader_common.c
INCLUDE_DIRS := \
../include \
--- /dev/null
+// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+#include <stdint.h>
+#include <stdio.h>
+#include "esp_err.h"
+
+esp_err_t bootloader_common_get_sha256_of_partition(uint32_t address, uint32_t size, int type, uint8_t *out_sha_256);
--- /dev/null
+// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "esp_err.h"
+
+esp_err_t bootloader_common_get_sha256_of_partition (uint32_t address, uint32_t size, int type, uint8_t *out_sha_256)
+{
+ return ESP_OK;
+}
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
+#include "esp_flash_partitions.h"
+#include "esp_partition.h"
#include "nvs.h"
#include "nvs_flash.h"
#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD
#define EXAMPLE_SERVER_URL CONFIG_FIRMWARE_UPG_URL
#define BUFFSIZE 1024
+#define HASH_LEN 32 /* SHA-256 digest length */
static const char *TAG = "native_ota_example";
/*an ota data write buffer ready to write to the flash*/
}
}
+void print_sha256 (const uint8_t *image_hash, const char *label)
+{
+ char hash_print[HASH_LEN * 2 + 1];
+ hash_print[HASH_LEN * 2] = 0;
+ for (int i = 0; i < HASH_LEN; ++i) {
+ sprintf(&hash_print[i * 2], "%02x", image_hash[i]);
+ }
+ ESP_LOGI(TAG, "%s: %s", label, hash_print);
+}
+
static void ota_example_task(void *pvParameter)
{
esp_err_t err;
http_cleanup(client);
task_fatal_error();
}
+
+ if (esp_partition_check_identity(esp_ota_get_running_partition(), update_partition) == true) {
+ ESP_LOGI(TAG, "The current running firmware is same as the firmware just downloaded");
+ int i = 0;
+ ESP_LOGI(TAG, "When a new firmware is available on the server, press the reset button to download it");
+ while(1) {
+ ESP_LOGI(TAG, "Waiting for a new firmware ... %d", ++i);
+ vTaskDelay(2000 / portTICK_PERIOD_MS);
+ }
+ }
+
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err));
void app_main()
{
+ uint8_t sha_256[HASH_LEN] = { 0 };
+ esp_partition_t partition;
+
+ // get sha256 digest for the partition table
+ partition.address = ESP_PARTITION_TABLE_OFFSET;
+ partition.size = ESP_PARTITION_TABLE_MAX_LEN;
+ partition.type = ESP_PARTITION_TYPE_DATA;
+ esp_partition_get_sha256(&partition, sha_256);
+ print_sha256(sha_256, "SHA-256 for the partition table: ");
+
+ // get sha256 digest for bootloader
+ partition.address = ESP_BOOTLOADER_OFFSET;
+ partition.size = ESP_PARTITION_TABLE_OFFSET;
+ partition.type = ESP_PARTITION_TYPE_APP;
+ esp_partition_get_sha256(&partition, sha_256);
+ print_sha256(sha_256, "SHA-256 for bootloader: ");
+
+ // get sha256 digest for running partition
+ esp_partition_get_sha256(esp_ota_get_running_partition(), sha_256);
+ print_sha256(sha_256, "SHA-256 for current firmware: ");
+
// Initialize NVS.
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {