Skip to content

Commit

Permalink
esp32/ota: Implement ESP-IDF OTA functionality.
Browse files Browse the repository at this point in the history
Implemented new functions:
* mark_app_invalid_rollback_and_reboot()
* check_rollback_is_possible()
* app_description()
* app_state()
* ota_begin()
* ota_write()
* ota_write_with_offset() for ESP-IDF version >= 4.2
* ota_end()
* ota_abort() for ESP-IDF version >= 4.3

* create tests
* update documentation

esp32/ota: Implement ESP-IDF OTA functionality.
  • Loading branch information
Emil Kondayan committed Feb 18, 2024
1 parent def6ad4 commit 7528feb
Show file tree
Hide file tree
Showing 3 changed files with 423 additions and 34 deletions.
145 changes: 142 additions & 3 deletions docs/library/esp32.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,154 @@ methods to enable over-the-air (OTA) updates.

.. classmethod:: Partition.mark_app_valid_cancel_rollback()

Signals that the current boot is considered successful.
Calling ``mark_app_valid_cancel_rollback`` is required on the first boot of a new
partition to avoid an automatic rollback at the next boot.
Signals that the current boot is considered successful by writing to the "otadata"
partition. Calling ``mark_app_valid_cancel_rollback`` is required on the first boot of a
new partition to avoid an automatic rollback at the next boot.
This uses the ESP-IDF "app rollback" feature with "CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE"
and an ``OSError(-261)`` is raised if called on firmware that doesn't have the
feature enabled.
It is OK to call ``mark_app_valid_cancel_rollback`` on every boot and it is not
necessary when booting firmware that was loaded using esptool.

.. classmethod:: Partition.mark_app_invalid_rollback_and_reboot()

Mark the current app partition invalid by writing to the "otadata"
partition, rollback to the previous workable app and then reboots.
If the rollback is sucessfull, the device will reset. If the flash does not have
at least one valid app (except the running app) then rollback is not possible.
If the "CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE" option is set, and a reset occurs without
calling either
``mark_app_valid_cancel_rollback()`` or ``mark_app_invalid_rollback_and_reboot()``
function then the application is rolled back.
This uses the ESP-IDF "app rollback" feature with "CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE"
and an ``OSError(-261)`` is raised if called on firmware that doesn't have the
feature enabled.

.. classmethod:: Partition.check_rollback_is_possible()

Returns True if at least one valid app is found(except the running one)
Returns False otherwise.

Checks if there is a bootable application on the slots which can be booted in case of
rollback. For an application to be considered bootable, the following conditions
must be met: the app must be marked to be valid(marked in otadata as not UNDEFINED,
INVALID or ABORTED and crc is good); must be marked bootable; secure_version of
app >= secure_version of efuse (if anti-rollback is enabled).

.. method:: Partition.app_description()

Returns a 7-tuple ``(secure_version, version, project_name, compile_time, compile_date,
idf_version, elf_sha256)`` which is a description of the app partition pointed by the
object.

If the object does not contain an app partition, OsError exception will be raised:
``ESP_ERR_NOT_FOUND`` no app description structure is found. Magic word is incorrect.
``ESP_ERR_NOT_SUPPORTED`` Partition is not application.
``ESP_ERR_INVALID_ARG`` Partition’s offset exceeds partition size.
``ESP_ERR_INVALID_SIZE`` Read would go out of bounds of the partition.

.. method:: Partition.app_state()

Returns the app state of a valid ota partition. It can be one of the following strings:
``new``: Monitor the first boot. In bootloader this state is changed to "pending verify"
``verify``: First boot for this app. If this state persists during second boot, then it
will be changed to ``aborted``
``valid``: App was confirmed as workable. App can boot and work without limits
``invalid``: App was confirmed as non-workable. This app will not be selected to
boot at all
``aborted``: App could not confirmed as workable or non-workable. In bootloader
"pending verify" state will be changed to ``aborted``. This app will not be selected
to boot at all
``undefined``: App can boot and work without limits

One of the following OsError can be raised:
``ESP_ERR_NOT_SUPPORTED``: Partition is not ota.
``ESP_ERR_NOT_FOUND``: Partition table does not have otadata or state was not found for
given partition.

.. method:: Partition.ota_begin(image_size)

Prepares the partition for an OTA update and start the process of updating.
The target partition is erased to the specified image size. If the size of the
artition is not known in advance, the entire partition is eraesd.

Note: This function is available since ESP-IDF version 4.3

Note: If the rollback option is enabled and the running application has the
"pending verify" state then it will lead to the ESP_ERR_OTA_ROLLBACK_INVALID_STATE error.
Confirm the running app before to run download a new app, use
mark_app_valid_cancel_rollback() function

``image_size``: The size of the image to be written. 0 indicates a partition of unknown
size. If you know the size of the partition in advance, you can pass the size in bytes.
The default value is "0"

Returns an integer handle, associated with the ota update process. The update
process must be ended by calling ``ota_end()`. Since ESP-IDF version 4.3,
an update process can also be ended by ``ota_abort()``.

An OsError can be raised if there is an error with the update process:
``ESP_ERR_INVALID_ARG``: Partition doesn’t point to an OTA app partition
``ESP_ERR_NO_MEM``: Cannot allocate memory for OTA operation
``ESP_ERR_OTA_PARTITION_CONFLICT``: Partition holds the currently running firmware,
cannot update in place
``ESP_ERR_NOT_FOUND``: Partition argument not found in partition table
``ESP_ERR_OTA_SELECT_INFO_INVALID``: The OTA data partition contains invalid data
``ESP_ERR_INVALID_SIZE``: Partition doesn’t fit in configured flash size
``ESP_ERR_FLASH_OP_TIMEOUT`` or ``ESP_ERR_FLASH_OP_FAIL``: Flash write failed
``ESP_ERR_OTA_ROLLBACK_INVALID_STATE``: If the running app has not confirmed state. Before
performing an update, the application must be valid

.. method:: Partition.ota_write(handle, buf)

Write OTA update data to the target partition. This function can be called multiple times
as data is received during the OTA operation. Data is written sequentially to the partition.

``handle``: The handle returned by ``ota_begin()``
``buf``: Data buffer to write

An OsError can be raised if there is an error with the update process:
``ESP_ERR_INVALID_ARG``: Handle is invalid
``ESP_ERR_OTA_VALIDATE_FAILED``: First byte of image contains invalid app image magic byte
``ESP_ERR_FLASH_OP_TIMEOUT`` or ``ESP_ERR_FLASH_OP_FAIL``: Flash write failed
``ESP_ERR_OTA_SELECT_INFO_INVALID``: OTA data partition has invalid contents

.. method:: Partition.ota_write_with_offset(handle, buffer, offset)

Write OTA update data to the target partition. This function writes data in non contiguous
manner. If flash encryption is enabled, data should be 16 byte aligned.

Note: This function is available since ESP-IDF version 4.2

Note: While performing OTA, if the packets arrive out of order, esp_ota_write_with_offset()
can be used to write data in non contiguous manner. Use of esp_ota_write_with_offset() in
combination with esp_ota_write() is not recommended.

An OsError can be raised if there is an error with the update process:
``ESP_ERR_INVALID_ARG``: handle is invalid
``ESP_ERR_OTA_VALIDATE_FAILED``: First byte of image contains invalid app image magic byte
``ESP_ERR_FLASH_OP_TIMEOUT`` or ``ESP_ERR_FLASH_OP_FAIL``: Flash write failed
``ESP_ERR_OTA_SELECT_INFO_INVALID``: OTA data partition has invalid contents

.. method:: Partition.ota_end(handle)

Finish the OTA update process and validate newly written app image.

An OsError can be raised if there is an error with the update process:
``ESP_ERR_NOT_FOUND``: OTA handle was not found
``ESP_ERR_INVALID_ARG``: Handle was never written to
``ESP_ERR_OTA_VALIDATE_FAILED``: OTA image is invalid (either not a valid app image, or
if secure boot is enabled - signature failed to verify)
``ESP_ERR_INVALID_STATE``: If flash encryption is enabled, this result indicates an internal
error writing the final encrypted bytes to flash

.. method:: Partition.ota_abort(handle)

Aborts the OTA process and frees resources

An OsError can be raised if there is an error:
``ESP_ERR_NOT_FOUND``: OTA handle was not found

Constants
~~~~~~~~~

Expand Down
138 changes: 138 additions & 0 deletions ports/esp32/esp32_partition.c
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,130 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_partition_mark_app_valid_cancel_rollback_
STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ(esp32_partition_mark_app_valid_cancel_rollback_obj,
MP_ROM_PTR(&esp32_partition_mark_app_valid_cancel_rollback_fun_obj));

STATIC mp_obj_t esp32_partition_mark_app_invalid_rollback_and_reboot(mp_obj_t cls_in) {
check_esp_err(esp_ota_mark_app_invalid_rollback_and_reboot());
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_partition_mark_app_invalid_rollback_and_reboot_fun_obj,
esp32_partition_mark_app_invalid_rollback_and_reboot);
STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ(esp32_mark_app_invalid_rollback_and_reboot_obj,
MP_ROM_PTR(&esp32_partition_mark_app_invalid_rollback_and_reboot_fun_obj));

STATIC mp_obj_t esp32_check_rollback_is_possible(mp_obj_t cls_in) {
return mp_obj_new_bool(esp_ota_check_rollback_is_possible());
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_check_rollback_is_possible_fun_obj, esp32_check_rollback_is_possible);
STATIC MP_DEFINE_CONST_CLASSMETHOD_OBJ(esp32_check_rollback_is_possible_obj, MP_ROM_PTR(&esp32_check_rollback_is_possible_fun_obj));

STATIC mp_obj_t esp32_app_description(mp_obj_t self_in) {
esp32_partition_obj_t *self = MP_OBJ_TO_PTR(self_in);
esp_app_desc_t app;

check_esp_err(esp_ota_get_partition_description(self->part, &app));

mp_obj_t tuple[] = {
mp_obj_new_int_from_uint(app.secure_version),
mp_obj_new_str(app.version, strlen(app.version)),
mp_obj_new_str(app.project_name, strlen(app.project_name)),
mp_obj_new_str(app.time, strlen(app.time)),
mp_obj_new_str(app.date, strlen(app.date)),
mp_obj_new_str(app.idf_ver, strlen(app.idf_ver)),
mp_obj_new_bytes(app.app_elf_sha256, 32)
};
return mp_obj_new_tuple(7, tuple);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_app_description_obj, esp32_app_description);

STATIC mp_obj_t esp32_app_get_state(mp_obj_t self_in) {
esp32_partition_obj_t *self = MP_OBJ_TO_PTR(self_in);
char *ret = NULL;
esp_ota_img_states_t state;

check_esp_err(esp_ota_get_state_partition(self->part, &state));

switch (state) {
// Monitor the first boot. In bootloader this state is changed to ESP_OTA_IMG_PENDING_VERIFY.
case ESP_OTA_IMG_NEW:
ret = "new";
break;
// First boot for this app. If this state persists during second boot, then it will be changed to ABORTED.
case ESP_OTA_IMG_PENDING_VERIFY:
ret = "verify";
break;
// App was confirmed as workable. App can boot and work without limits.
case ESP_OTA_IMG_VALID:
ret = "valid";
break;
// App was confirmed as non-workable. This app will not be selected to boot at all.
case ESP_OTA_IMG_INVALID:
ret = "invalid";
break;
// App could not confirmed as workable or non-workable. In bootloader IMG_PENDING_VERIFY state will be changed to IMG_ABORTED. This app will not be selected to boot at all.
case ESP_OTA_IMG_ABORTED:
ret = "aborted";
break;
// App can boot and work without limits.
default:
ret = "undefined";
}
return mp_obj_new_str(ret, strlen(ret));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_app_get_state_obj, esp32_app_get_state);

STATIC mp_obj_t esp32_ota_begin(size_t n_args, const mp_obj_t *args) {
esp32_partition_obj_t *self = MP_OBJ_TO_PTR(args[0]);
esp_ota_handle_t handle;
size_t image_size = 0;

if (n_args == 2) {
image_size = mp_obj_get_int(args[1]);
}
check_esp_err(esp_ota_begin(self->part, image_size, &handle));
return mp_obj_new_int_from_uint(handle);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp32_ota_begin_obj, 1, 2, esp32_ota_begin);

STATIC mp_obj_t esp32_ota_write(mp_obj_t self_in, const mp_obj_t handle_in, const mp_obj_t data_in) {
const esp_ota_handle_t handle = mp_obj_get_int(handle_in);
mp_buffer_info_t bufinfo;
mp_get_buffer_raise(data_in, &bufinfo, MP_BUFFER_READ);

check_esp_err(esp_ota_write(handle, bufinfo.buf, bufinfo.len));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_3(esp32_ota_write_obj, esp32_ota_write);

#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)
STATIC mp_obj_t esp32_ota_write_with_offset(size_t n_args, const mp_obj_t *args) {
esp_ota_handle_t handle = mp_obj_get_int(args[1]);
mp_buffer_info_t bufinfo;
mp_get_buffer_raise(args[2], &bufinfo, MP_BUFFER_READ);
const uint32_t offset = mp_obj_get_int(args[3]);

check_esp_err(esp_ota_write_with_offset(handle, bufinfo.buf, bufinfo.len, offset));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp32_ota_write_with_offset_obj, 4, 4, esp32_ota_write_with_offset);
#endif

STATIC mp_obj_t esp32_ota_end(mp_obj_t self_in, const mp_obj_t handle_in) {
const esp_ota_handle_t handle = mp_obj_get_int(handle_in);

check_esp_err(esp_ota_end(handle));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp32_ota_end_obj, esp32_ota_end);

#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0)
STATIC mp_obj_t esp32_ota_abort(mp_obj_t self_in, const mp_obj_t handle_in) {
esp_ota_handle_t handle = mp_obj_get_int(handle_in);

check_esp_err(esp_ota_abort(handle));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp32_ota_abort_obj, esp32_ota_abort);
#endif

STATIC const mp_rom_map_elem_t esp32_partition_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_find), MP_ROM_PTR(&esp32_partition_find_obj) },

Expand All @@ -275,8 +399,22 @@ STATIC const mp_rom_map_elem_t esp32_partition_locals_dict_table[] = {

{ MP_ROM_QSTR(MP_QSTR_set_boot), MP_ROM_PTR(&esp32_partition_set_boot_obj) },
{ MP_ROM_QSTR(MP_QSTR_mark_app_valid_cancel_rollback), MP_ROM_PTR(&esp32_partition_mark_app_valid_cancel_rollback_obj) },
{ MP_ROM_QSTR(MP_QSTR_mark_app_invalid_rollback_and_reboot), MP_ROM_PTR(&esp32_mark_app_invalid_rollback_and_reboot_obj) },
{ MP_ROM_QSTR(MP_QSTR_check_rollback_is_possible), MP_ROM_PTR(&esp32_check_rollback_is_possible_obj) },
{ MP_ROM_QSTR(MP_QSTR_get_next_update), MP_ROM_PTR(&esp32_partition_get_next_update_obj) },

{ MP_ROM_QSTR(MP_QSTR_app_description), MP_ROM_PTR(&esp32_app_description_obj) },
{ MP_ROM_QSTR(MP_QSTR_app_state), MP_ROM_PTR(&esp32_app_get_state_obj) },
{ MP_ROM_QSTR(MP_QSTR_ota_begin), MP_ROM_PTR(&esp32_ota_begin_obj) },
{ MP_ROM_QSTR(MP_QSTR_ota_write), MP_ROM_PTR(&esp32_ota_write_obj) },
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)
{ MP_ROM_QSTR(MP_QSTR_ota_write_with_offset), MP_ROM_PTR(&esp32_ota_write_with_offset_obj) },
#endif
{ MP_ROM_QSTR(MP_QSTR_ota_end), MP_ROM_PTR(&esp32_ota_end_obj) },
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0)
{ MP_ROM_QSTR(MP_QSTR_ota_abort), MP_ROM_PTR(&esp32_ota_abort_obj) },
#endif

{ MP_ROM_QSTR(MP_QSTR_BOOT), MP_ROM_INT(ESP32_PARTITION_BOOT) },
{ MP_ROM_QSTR(MP_QSTR_RUNNING), MP_ROM_INT(ESP32_PARTITION_RUNNING) },
{ MP_ROM_QSTR(MP_QSTR_TYPE_APP), MP_ROM_INT(ESP_PARTITION_TYPE_APP) },
Expand Down
Loading

0 comments on commit 7528feb

Please sign in to comment.