ESP32 OTA#

Deployed applications occasionally need updating. Processors with wireless connectivity can do this over the air (OTA). In the case of MicroPython, both the Python sources and the interpreter may need updating. Here we focus on the latter - installing a new MicroPython virtual machine.

The MicroPython port for the ESP32 exposes an API for this purpose.

The first step is to compile and flash a version of MicroPython with OTA enabled. Doing so chooses a flash layout with space for copies of the Python VM, ota_0 and ota_1, defined in ~/micropython/ports/esp32/partitions-ota.csv.

Display available and the currently active partition:

%connect esp32
from esp32 import Partition

for p in Partition.find(Partition.TYPE_APP):
    print(p)
    
print("active partition:", Partition(Partition.RUNNING).info()[4])
Connected to esp32 @ serial:///dev/ttyUSB0
<Partition type=0, subtype=16, address=65536, size=1572864, label=ota_0, encrypted=0>
<Partition type=0, subtype=17, address=1638400, size=1572864, label=ota_1, encrypted=0>
active partition: ota_1

During updating, the VM runs from the currently active partition, while a new VM is written to the other partition. Upon restart, the new VM is invoked, and, if successful, is marked as the new “active” partition. In case of error, subsequent reboots revert to the previous, working partition.

Download and install the ota32 package.

%rsync
UPDATE  /lib/ota32/ota.py

The update process takes two arguments: a url pointing to the code to be uploaded, and the “sha”, a secure hash of the code.

The compiled binaries (~/micropython/ports/esp32/build-GENERIC_OTA) include two versions: firmware.bin and micropython.bin. The former contains all the flash partitions and is used for the (initial) installation with the esptool. The latter contains just the MicroPython VM and is used for the OTA update.

%%bash

cd ~/micropython/ports/esp32/build-GENERIC_OTA/
ls -l *bin

# compute the sha
sha256sum micropython.bin >micropython.sha
cat micropython.sha
-rw-r--r-- 1 iot gpio 1509504 Jul 24 14:29 firmware.bin
-rw-r--r-- 1 iot gpio 1448064 Jul 22 17:48 micropython.bin
-rw-r--r-- 1 iot gpio    8192 Jul 22 17:42 ota_data_initial.bin
76617fb397c516c30d3959c5778a1fc61a7e5ec83f6128bc58feb84933f4d0c2  micropython.bin

Copy both files to a secure server. For demonstration we’ll use the built-in webserver of ide49.

%%bash

cd ~/micropython/ports/esp32/build-GENERIC_OTA/
mkdir -p /service-config/nginx/html/ota
cd /service-config/nginx/html/ota
cp micropython.bin .
cp micropython.sha .
%rsync
%softreset
UPDATE  /lib/ota32/open_url.py

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!   softreset ...     !!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Download the “new” firmware (edit the server url):

from ota32 import OTA, open_url
import gc

gc.collect()
print(gc.mem_free())

# url of webserver (e.g. iot49.local)
server = 'https://server.local'

# read the sha
s = open_url('{}/ota/micropython.sha'.format(server))
sha = s.read(1024).split()[0].decode()
s.close()
             
gc.collect()

# explicitly state sha in case of memory error
# sha = '76617fb397c516c30d3959c5778a1fc61a7e5ec83f6128bc58feb84933f4d0c2'
url = '{}/ota/micropython.bin'.format(server)

print("flashing {} with\nsha {}".format(url, sha))
ota = OTA(verbose=True)
ota.ota(url, sha)
103600
flashing https://server.local/ota/micropython.bin with
sha 76617fb397c516c30d3959c5778a1fc61a7e5ec83f6128bc58feb84933f4d0c2
OTA .................................................................................................................................................................................................................................................................................................................................................................. Done.

Reboot the processor:

import machine
machine.reset()
ets Jun  8 2016 00:22:57

rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:4
load:0x3fff0034,len:5524
load:0x40078000,len:12800
load:0x40080400,len:4292
entry 0x400806b0
Connecting to WLAN ... time (2021, 8, 1, 19, 32, 50, 6, 213)
WebREPL daemon started on ws://10.39.40.135:8266
Started webrepl in normal mode
MicroPython v1.16-78-ge3291e180 on 2021-07-22; 4MB/OTA module with ESP32
Type "help()" for more information.
>>> 
Interrupted

Now the “other” partition is active:

from esp32 import Partition

print("active partition:", Partition(Partition.RUNNING).info()[4])
active partition: ota_0

Run tests as appropriate to verify that the new app works as expected. Then run the code below to mark it as the new default, started after every boot:

from esp32 import Partition

# OTA ... accept uploaded image (if we uploaded a new one)
from esp32 import Partition
Partition.mark_app_valid_cancel_rollback()

You could add these statements to boot.py, possibly conditioned on passing appropriate tests. Note that Partition.mark_app_valid_cancel_rollback() has an effect only if new firmware has been uploaded.