glenn20 / micropython-esp32-ota

MIT License
39 stars 10 forks source link

Micropython OTA tools for ESP32 devices

Some classes and tools for Over-The-Air (OTA) firmware updates on ESP32. I wanted a simple and flexible interface for managing and running OTA updates for ESP32* devices. These tools are for managing OTA updates of the micropython firmware installed in the device flash storage (not the python files in the mounted filesystem).

Usage

Write a new micropython image from a web server to the next OTA partition on the flash storage:

>>> import ota.update
>>> ota.update.from_file("http://nas.local/micropython.bin", reboot=True)
Writing new micropython image to OTA partition 'ota_0'...
Device capacity: 384 x 4096 byte blocks.
Opening firmware file http://nas.local/micropython.bin...
Writing 380 blocks + 2032 bytes.
BLOCK 380 + 2032 bytes
Verifying SHA of the written data...Passed.
SHA256=7920d527d578e90ce074b23f9050ffe4ebbd8809b79da0b81493f6ba721d110e
OTA Partition 'ota_0' updated successfully.
Micropython will boot from 'ota_0' partition on next boot.
Remember to call ota.rollback.cancel() after successful reboot.
Rebooting in 10 seconds (ctrl-C to cancel)

Print the current status of the OTA partitions on the device:

>>> import ota.status
>>> ota.status.status()
Micropython firmware v1.20.0 has booted from partition 'ota_0'.
The next OTA partition is 'ota_1'.
The / filesystem is mounted from partition 'vfs'.
Partition table:
# Name       Type     SubType      Offset       Size (bytes)
  nvs        data     nvs          0x9000     0x4000     16,384
  otadata    data     ota          0xd000     0x2000      8,192
  phy_init   data     phy          0xf000     0x1000      4,096
  ota_0      app      ota_0       0x10000   0x180000  1,572,864
  ota_1      app      ota_1      0x190000   0x180000  1,572,864
  vfs        data     fat        0x310000    0xf0000    983,040
>>>

NOTE: After performing an OTA update, the device must be hard_reset() or power cycled before it will boot into the new firmware. A soft_reset() (including pressing ctrl-D at the repl prompt) will not load the new firmware.

After booting up successfully, stop the esp32 from rolling back to the previous firmware on next boot (should do this on every successful boot and app startup):

import ota.rollback
ota.rollback.cancel()

Installation

Install ota package with mpremote into /lib/ota/ on the device (as .py modules):

mpremote mip install github:glenn20/micropython-esp32-ota/mip/ota

or, install module as byte-compiled .mpy files

mpremote mip install github:glenn20/micropython-esp32-ota/mip/ota/mpy

Remember to ensure /lib is in your sys.path.

How it works

An OTA-enabled partition table

To support Over-The-Air updates, a micropython image requires a special partition table, such as:

Partition table:
# Name       Type     SubType      Offset       Size (bytes)
  nvs        data     nvs          0x9000     0x4000     16,384
  otadata    data     ota          0xd000     0x2000      8,192
  phy_init   data     phy          0xf000     0x1000      4,096
  ota_0      app      ota_0       0x10000   0x180000  1,572,864
  ota_1      app      ota_1      0x190000   0x180000  1,572,864
  vfs        data     fat        0x310000    0xf0000    983,040

For micropython, an OTA-enabled partition table usually includes:

Any micropython image built with BOARD_VARIANT=OTA will have a partition table like this (including the official OTA images at https://micropython.org/download/ESP32_GENERIC).

You can also add an OTA-enabled partition table to a non-ota micropython firmware file with mp-image-tool-esp32 --ota.

Writing new firmware into the ota partitions

The partition table has to make room for two app partitions on the device (instead of the normal one). This means space is tight on a 4MB flash device. The OTA partition usually has less room for each micropython firmware image (1.5MB instead of 2MB) and much less room for the vfs filesystem partition (<1MB instead of 2MB). Devices with more than 4MB of flash can use larger app and vfs partitions.

Micropython will boot from one of the ota_X app partitions and write new firmware to the other one. After writing new firmware to the other partition, it will be set as the boot partition for the next reboot. The old firmware image is still available in case it is necessary to rollback to the previous firmware.

After booting from either ota partition, the micropython firmware will automatically mount the / filesystem from the vfs partition.

Micropython firmware for OTA updates

An OTA partition should be updated with a "micropython app image". The micropython firmware (.bin files) downloaded from the MicroPython downloads page combine the bootloader, partition table and the micropython app image, so can not be used for micropython OTA updates.

There are three ways to obtain a micropython.bin you can use for OTA updates:

  1. Download a .app-bin file from the MicroPython downloads page,
  2. Use the micropython.bin file in the ports/esp32/build_XXX folder
    • if you build your own micropython firmware, or
  3. Extract the .app-bin firmware from a combined firmware file with:

API docs

ota.update module

The ota.update module provides the OTA class which can be used to write new micropython firmware to the next ota partition on the device and two convenience functions which use OTA to perform simple OTA firmware updates: from_file() and from_json().

The functions above use the methods in the OTA class to perform the OTA updates.

Examples

import ota.update

# Write firmware from a url provided in a JSON file
ota.update.from_json("http://nas.local/micropython/micropython.json")

# Write firmware from a url or filename and reboot if successful and verified
ota.update.from_firmware_file(
    "http://nas.local/micropython/micropython.bin",
    sha="7920d527d578e90ce074b23f9050ffe4ebbd8809b79da0b81493f6ba721d110e",
    length=1558512)

# Write firmware from an open stream:
with ota.update.OTA() as ota:
    with open("/sdcard/micropython.bin", "rb") as f:
        ota.from_stream(f)

# Read a firmware file from a serial uart
remaining = 1558512
sha = "7920d527d578e90ce074b23f9050ffe4ebbd8809b79da0b81493f6ba721d110e"
with ota.update.OTA(length=remaining, sha=sha) as ota:
    data = memoryview(bytearray(1024))
    gc.collect()
    while remaining > 0:
        n = uart.readinto(data[:min(remaining, len(data))]):
        ota.write(data[:n])
        remaining -= n

# Used without the "with" statement - must call close() explicitly
ota = ota.update.OTA()
ota.from_json("http://nas.local/micropython/micropython.json")
ota.close()

ota.rollback module

When booting a new OTA firmware for the first time, you need to tell the bootloader if it is OK to continue using the new firmware. Otherwise, the bootloader will assume something went wrong and automatically rollback to the previous firmware on the next reboot. You can use ota.rollback.cancel() to tell the bootloader not to rollback to the previous firmware.

If the new firmware fails to startup or your app does not operate correctly with the new firmware, reboot the device without cancelling the rollback and the old firmware will be restored. You may also wish to use a watchdog timer (WDT) during your app startup sequence to force a reboot if the startup hangs or fails before you call ota.rollback.cancel().

Note: the rollback mechanism is only available if the bootloader was compiled with CONFIG_BOOTLOADER_ROLLBACK_ENABLE=y.

ota.status module