esp32: Improve support for OTA updates.
This commit adds several small items to improve the support for OTA updates on an esp32: - a partition table for 4MB flash modules that has two OTA partitions ready to go to do updates - a GENERIC_OTA board that uses that partition table and that enables automatic roll-back in the bootloader - a new esp32.Partition.mark_app_valid_cancel_rollback() class-method to signal that the boot is successful and should not be rolled back at the next reset - an automated test for doing an OTA update - documentation updates
This commit is contained in:
parent
7d97d241e8
commit
952ff8a8ea
|
@ -65,7 +65,8 @@ Functions
|
||||||
Flash partitions
|
Flash partitions
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
This class gives access to the partitions in the device's flash memory.
|
This class gives access to the partitions in the device's flash memory and includes
|
||||||
|
methods to enable over-the-air (OTA) updates.
|
||||||
|
|
||||||
.. class:: Partition(id)
|
.. class:: Partition(id)
|
||||||
|
|
||||||
|
@ -75,7 +76,8 @@ This class gives access to the partitions in the device's flash memory.
|
||||||
.. classmethod:: Partition.find(type=TYPE_APP, subtype=0xff, label=None)
|
.. classmethod:: Partition.find(type=TYPE_APP, subtype=0xff, label=None)
|
||||||
|
|
||||||
Find a partition specified by *type*, *subtype* and *label*. Returns a
|
Find a partition specified by *type*, *subtype* and *label*. Returns a
|
||||||
(possibly empty) list of Partition objects.
|
(possibly empty) list of Partition objects. Note: ``subtype=0xff`` matches any subtype
|
||||||
|
and ``label=None`` matches any label.
|
||||||
|
|
||||||
.. method:: Partition.info()
|
.. method:: Partition.info()
|
||||||
|
|
||||||
|
@ -98,6 +100,19 @@ This class gives access to the partitions in the device's flash memory.
|
||||||
.. method:: Partition.get_next_update()
|
.. method:: Partition.get_next_update()
|
||||||
|
|
||||||
Gets the next update partition after this one, and returns a new Partition object.
|
Gets the next update partition after this one, and returns a new Partition object.
|
||||||
|
Typical usage is ``Partition(Partition.RUNNING).get_next_update()``
|
||||||
|
which returns the next partition to update given the current running one.
|
||||||
|
|
||||||
|
.. 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.
|
||||||
|
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 firmare that was loaded using esptool.
|
||||||
|
|
||||||
Constants
|
Constants
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
@ -105,12 +120,16 @@ Constants
|
||||||
.. data:: Partition.BOOT
|
.. data:: Partition.BOOT
|
||||||
Partition.RUNNING
|
Partition.RUNNING
|
||||||
|
|
||||||
Used in the `Partition` constructor to fetch various partitions.
|
Used in the `Partition` constructor to fetch various partitions: ``BOOT`` is the
|
||||||
|
partition that will be booted at the next reset and ``RUNNING`` is the currently
|
||||||
|
running partition.
|
||||||
|
|
||||||
.. data:: Partition.TYPE_APP
|
.. data:: Partition.TYPE_APP
|
||||||
Partition.TYPE_DATA
|
Partition.TYPE_DATA
|
||||||
|
|
||||||
Used in `Partition.find` to specify the partition type.
|
Used in `Partition.find` to specify the partition type: ``APP`` is for bootable
|
||||||
|
firmware partitions (typically labelled ``factory``, ``ota_0``, ``ota_1``), and
|
||||||
|
``DATA`` is for other partitions, e.g. ``nvs``, ``otadata``, ``phy_init``, ``vfs``.
|
||||||
|
|
||||||
.. data:: HEAP_DATA
|
.. data:: HEAP_DATA
|
||||||
HEAP_EXEC
|
HEAP_EXEC
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
#define MICROPY_HW_BOARD_NAME "4MB/OTA module"
|
||||||
|
#define MICROPY_HW_MCU_NAME "ESP32"
|
|
@ -0,0 +1,4 @@
|
||||||
|
SDKCONFIG += boards/sdkconfig.base
|
||||||
|
SDKCONFIG += boards/GENERIC_OTA/sdkconfig.board
|
||||||
|
|
||||||
|
PART_SRC = partitions-ota.csv
|
|
@ -0,0 +1,4 @@
|
||||||
|
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||||
|
|
||||||
|
# ESP-IDF v3:
|
||||||
|
CONFIG_APP_ROLLBACK_ENABLE=y
|
|
@ -209,6 +209,15 @@ STATIC mp_obj_t esp32_partition_get_next_update(mp_obj_t self_in) {
|
||||||
}
|
}
|
||||||
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_partition_get_next_update_obj, esp32_partition_get_next_update);
|
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_partition_get_next_update_obj, esp32_partition_get_next_update);
|
||||||
|
|
||||||
|
STATIC mp_obj_t esp32_partition_mark_app_valid_cancel_rollback(mp_obj_t cls_in) {
|
||||||
|
check_esp_err(esp_ota_mark_app_valid_cancel_rollback());
|
||||||
|
return mp_const_none;
|
||||||
|
}
|
||||||
|
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp32_partition_mark_app_valid_cancel_rollback_fun_obj,
|
||||||
|
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 const mp_rom_map_elem_t esp32_partition_locals_dict_table[] = {
|
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) },
|
{ MP_ROM_QSTR(MP_QSTR_find), MP_ROM_PTR(&esp32_partition_find_obj) },
|
||||||
|
|
||||||
|
@ -218,6 +227,7 @@ STATIC const mp_rom_map_elem_t esp32_partition_locals_dict_table[] = {
|
||||||
{ MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&esp32_partition_ioctl_obj) },
|
{ MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&esp32_partition_ioctl_obj) },
|
||||||
|
|
||||||
{ MP_ROM_QSTR(MP_QSTR_set_boot), MP_ROM_PTR(&esp32_partition_set_boot_obj) },
|
{ 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_get_next_update), MP_ROM_PTR(&esp32_partition_get_next_update_obj) },
|
{ MP_ROM_QSTR(MP_QSTR_get_next_update), MP_ROM_PTR(&esp32_partition_get_next_update_obj) },
|
||||||
|
|
||||||
{ MP_ROM_QSTR(MP_QSTR_BOOT), MP_ROM_INT(ESP32_PARTITION_BOOT) },
|
{ MP_ROM_QSTR(MP_QSTR_BOOT), MP_ROM_INT(ESP32_PARTITION_BOOT) },
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Partition table for MicroPython with OTA support using 4MB flash
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
|
||||||
|
nvs, data, nvs, 0x9000, 0x4000,
|
||||||
|
otadata, data, ota, 0xd000, 0x2000,
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
|
ota_0, app, ota_0, 0x10000, 0x180000,
|
||||||
|
ota_1, app, ota_1, 0x190000, 0x180000,
|
||||||
|
vfs, data, fat, 0x310000, 0x0f0000,
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Test ESP32 OTA updates, including automatic roll-back.
|
||||||
|
# Running this test requires firmware with an OTA Partition, such as the GENERIC_OTA "board".
|
||||||
|
# This test also requires patience as it copies the boot partition into the other OTA slot.
|
||||||
|
|
||||||
|
import machine
|
||||||
|
from esp32 import Partition
|
||||||
|
|
||||||
|
# start by checking that the running partition table has OTA partitions, 'cause if
|
||||||
|
# it doesn't there's nothing we can test
|
||||||
|
cur = Partition(Partition.RUNNING)
|
||||||
|
cur_name = cur.info()[4]
|
||||||
|
if not cur_name.startswith("ota_"):
|
||||||
|
print("SKIP")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
|
def log(*args):
|
||||||
|
if DEBUG:
|
||||||
|
print(*args)
|
||||||
|
|
||||||
|
|
||||||
|
# replace boot.py with the test code that will run on each reboot
|
||||||
|
import uos
|
||||||
|
|
||||||
|
try:
|
||||||
|
uos.rename("boot.py", "boot-orig.py")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
with open("boot.py", "w") as f:
|
||||||
|
f.write("DEBUG=" + str(DEBUG))
|
||||||
|
f.write(
|
||||||
|
"""
|
||||||
|
import machine
|
||||||
|
from esp32 import Partition
|
||||||
|
cur = Partition(Partition.RUNNING)
|
||||||
|
cur_name = cur.info()[4]
|
||||||
|
|
||||||
|
def log(*args):
|
||||||
|
if DEBUG: print(*args)
|
||||||
|
|
||||||
|
from step import STEP, EXPECT
|
||||||
|
log("Running partition: " + cur_name + " STEP=" + str(STEP) + " EXPECT=" + EXPECT)
|
||||||
|
if cur_name != EXPECT:
|
||||||
|
print("\\x04FAILED: step " + str(STEP) + " expected " + EXPECT + " got " + cur_name + "\\x04")
|
||||||
|
|
||||||
|
if STEP == 0:
|
||||||
|
log("Not confirming boot ok and resetting back into first")
|
||||||
|
nxt = cur.get_next_update()
|
||||||
|
with open("step.py", "w") as f:
|
||||||
|
f.write("STEP=1\\nEXPECT=\\"" + nxt.info()[4] + "\\"\\n")
|
||||||
|
machine.reset()
|
||||||
|
elif STEP == 1:
|
||||||
|
log("Booting into second partition again")
|
||||||
|
nxt = cur.get_next_update()
|
||||||
|
nxt.set_boot()
|
||||||
|
with open("step.py", "w") as f:
|
||||||
|
f.write("STEP=2\\nEXPECT=\\"" + nxt.info()[4] + "\\"\\n")
|
||||||
|
machine.reset()
|
||||||
|
elif STEP == 2:
|
||||||
|
log("Confirming boot ok and rebooting into same partition")
|
||||||
|
Partition.mark_app_valid_cancel_rollback()
|
||||||
|
with open("step.py", "w") as f:
|
||||||
|
f.write("STEP=3\\nEXPECT=\\"" + cur_name + "\\"\\n")
|
||||||
|
machine.reset()
|
||||||
|
elif STEP == 3:
|
||||||
|
log("Booting into original partition")
|
||||||
|
nxt = cur.get_next_update()
|
||||||
|
nxt.set_boot()
|
||||||
|
with open("step.py", "w") as f:
|
||||||
|
f.write("STEP=4\\nEXPECT=\\"" + nxt.info()[4] + "\\"\\n")
|
||||||
|
machine.reset()
|
||||||
|
elif STEP == 4:
|
||||||
|
log("Confirming boot ok and DONE!")
|
||||||
|
Partition.mark_app_valid_cancel_rollback()
|
||||||
|
import uos
|
||||||
|
uos.remove("step.py")
|
||||||
|
uos.remove("boot.py")
|
||||||
|
uos.rename("boot-orig.py", "boot.py")
|
||||||
|
print("\\nSUCCESS!\\n\\x04\\x04")
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_partition(src, dest):
|
||||||
|
log("Partition copy: {} --> {}".format(src.info(), dest.info()))
|
||||||
|
sz = src.info()[3]
|
||||||
|
if dest.info()[3] != sz:
|
||||||
|
raise ValueError("Sizes don't match: {} vs {}".format(sz, dest.info()[3]))
|
||||||
|
addr = 0
|
||||||
|
blk = bytearray(4096)
|
||||||
|
while addr < sz:
|
||||||
|
if sz - addr < 4096:
|
||||||
|
blk = blk[: sz - addr]
|
||||||
|
if addr & 0xFFFF == 0:
|
||||||
|
# need to show progress to run-tests else it times out
|
||||||
|
print(" ... 0x{:06x}".format(addr))
|
||||||
|
src.readblocks(addr >> 12, blk)
|
||||||
|
dest.writeblocks(addr >> 12, blk)
|
||||||
|
addr += len(blk)
|
||||||
|
|
||||||
|
|
||||||
|
# get things started by copying the current partition into the next slot and rebooting
|
||||||
|
print("Copying current to next partition")
|
||||||
|
nxt = cur.get_next_update()
|
||||||
|
copy_partition(cur, nxt)
|
||||||
|
print("Partition copied, booting into it")
|
||||||
|
nxt.set_boot()
|
||||||
|
|
||||||
|
# the step.py file is used to keep track of state across reboots
|
||||||
|
# EXPECT is the name of the partition we expect to reboot into
|
||||||
|
with open("step.py", "w") as f:
|
||||||
|
f.write('STEP=0\nEXPECT="' + nxt.info()[4] + '"\n')
|
||||||
|
|
||||||
|
machine.reset()
|
|
@ -0,0 +1,15 @@
|
||||||
|
Copying current to next partition
|
||||||
|
########
|
||||||
|
Partition copied, booting into it
|
||||||
|
########
|
||||||
|
Not confirming boot ok and resetting back into first
|
||||||
|
########
|
||||||
|
Booting into second partition again
|
||||||
|
########
|
||||||
|
Confirming boot ok and rebooting into same partition
|
||||||
|
########
|
||||||
|
Booting into original partition
|
||||||
|
########
|
||||||
|
Confirming boot ok and DONE!
|
||||||
|
|
||||||
|
SUCCESS!
|
|
@ -56,6 +56,7 @@ def run_micropython(pyb, args, test_file, is_special=False):
|
||||||
special_tests = (
|
special_tests = (
|
||||||
'micropython/meminfo.py', 'basics/bytes_compare3.py',
|
'micropython/meminfo.py', 'basics/bytes_compare3.py',
|
||||||
'basics/builtin_help.py', 'thread/thread_exc2.py',
|
'basics/builtin_help.py', 'thread/thread_exc2.py',
|
||||||
|
'esp32/partition_ota.py',
|
||||||
)
|
)
|
||||||
had_crash = False
|
had_crash = False
|
||||||
if pyb is None:
|
if pyb is None:
|
||||||
|
|
Loading…
Reference in New Issue