From 7f7a9272f0f1aae4721c688f31f47957e37cb527 Mon Sep 17 00:00:00 2001 From: Renz Christian Bagaporo Date: Mon, 27 May 2019 11:10:00 +0800 Subject: [PATCH] examples: create example for both Python and CLI otatool interfaces --- examples/system/ota/otatool/README.md | 57 ++--- .../ota/otatool/get_running_partition.py | 87 ++++++++ .../system/ota/otatool/otatool_example.py | 194 ++++++------------ .../system/ota/otatool/otatool_example.sh | 95 +++++++++ tools/ci/executable-list.txt | 2 + 5 files changed, 275 insertions(+), 160 deletions(-) create mode 100644 examples/system/ota/otatool/get_running_partition.py create mode 100644 examples/system/ota/otatool/otatool_example.sh diff --git a/examples/system/ota/otatool/README.md b/examples/system/ota/otatool/README.md index c14e1cfb0d..9b52e8952c 100644 --- a/examples/system/ota/otatool/README.md +++ b/examples/system/ota/otatool/README.md @@ -6,7 +6,9 @@ This example demonstrates common operations the OTA tool [otatool.py](../../../c - switching boot partitions, and - switching to factory partition. -Users taking a look at this example should focus on the contents of the python script [otatool_example.py](otatool_example.py). The script contains programmatic invocations of the tool [otatool.py](../../../components/app_update/otatool.py) in Python for the operations mentioned above; and can serve as a guide for users wanting to do the same in their applications. +Users taking a look at this example should focus on the contents of the Python script [otatool_example.py](otatool_example.py) or shell script [otatool_example.sh](otatool_example.sh). The scripts contain +programmatic invocation of the tool's functions via the Python API and command-line interface, respectively. Note +that on Windows, the shell script example requires a POSIX-compatible environment via MSYS2/Git Bash/WSL etc. The built application in this example outputs the currently running partition, whose output is used to verify if the tool switched OTA partitions succesfully. The built application binary is written to all OTA partitions at the start of the example to be able to determine the running @@ -16,38 +18,46 @@ partition for all switches performed. ### Build and Flash -Before running the example script [otatool_example.py](otatool_example.py), it is necessary to build and flash the firmware using the usual means: +Before running either of the example scripts, it is necessary to build and flash the firmware using the usual means: +Make: ```bash -# If using Make make build flash +``` -# If using CMake +CMake: +```bash idf.py build flash ``` ### Running [otatool_example.py](otatool_example.py) -The example can be executed by running the script [otatool_example.py](otatool_example.py). Either run it directly using - -```bash -./otatool_example.py -``` - -or run it using +The example can be executed by running the script [otatool_example.py](otatool_example.py) or [otatool_example.sh](otatool_example.sh). +Python script: ```bash python otatool_example.py ``` -The script searches for valid target devices connected to the host and performs the operations on the first one it finds. This could present problems if there -are multiple viable target devices attached to the host. To perform the operations on a specific device, specify the port it is attached to during script invocation: +Shell script: +``` +./otatool_example.sh +``` +The script searches for valid target devices connected to the host and performs the operations on the first one it finds. This could present problems if there +are multiple viable target devices attached to the host. To perform the operations on a specific device, specify the port it is attached to during script invocation ("/dev/ttyUSB2" for example): + +Python script: ```bash -# The target device is attached to /dev/ttyUSB2, for example python otatool_example.py --port /dev/ttyUSB2 ``` + +Shell script: +``` +./otatool_example.sh /dev/ttyUSB2 +``` + ## Example output Running the script produces the following output: @@ -55,16 +65,13 @@ Running the script produces the following output: ``` Writing factory firmware to ota_0 Writing factory firmware to ota_1 -Checking written firmware to ota_0 and ota_1 match factory firmware -Switching to ota partition name factory -Switching to ota partition name factory -Switching to ota partition slot 0 -Switching to ota partition name ota_1 -Switching to ota partition slot 1 -Switching to ota partition name ota_0 -Switching to ota partition slot 0 -Switching to ota partition name factory -Switching to ota partition slot 1 +Switching to factory app +Switching to OTA slot 0 +Switching to OTA slot 1 (twice in a row) +Switching to OTA slot 0 (twice in a row) +Switching to factory app +Switching to OTA slot 1 + +Partition tool operations performed successfully -OTA tool operations executed successfully! ``` diff --git a/examples/system/ota/otatool/get_running_partition.py b/examples/system/ota/otatool/get_running_partition.py new file mode 100644 index 0000000000..b91d71a6d4 --- /dev/null +++ b/examples/system/ota/otatool/get_running_partition.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# +# Demonstrates the use of otatool.py, a tool for performing ota partition level +# operations. +# +# 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. +import os +import sys +import serial +import subprocess +import re +import argparse + +from subprocess import CalledProcessError + + +def get_running_partition(port=None): + # Monitor the serial output of target device. The firmware outputs the currently + # running partition + + IDF_PATH = os.path.expandvars("$IDF_PATH") + sys.path.append(os.path.join(IDF_PATH, 'components', 'esptool_py', 'esptool')) + import esptool + + ESPTOOL_PY = os.path.join(IDF_PATH, "components", "esptool_py", "esptool", "esptool.py") + + baud = os.environ.get("ESPTOOL_BAUD", esptool.ESPLoader.ESP_ROM_BAUD) + + if not port: + error_message = "Unable to obtain default target device port.\nSerial log:\n\n" + try: + # Check what esptool.py finds on what port the device is connected to + output = subprocess.check_output([sys.executable, ESPTOOL_PY, "chip_id"]) # may raise CalledProcessError + pattern = r"Serial port ([\S]+)" + pattern = re.compile(pattern.encode()) + + port = re.search(pattern, output).group(1) # may raise AttributeError + except CalledProcessError as e: + raise Exception(error_message + e.output) + except AttributeError: + raise Exception(error_message + output) + + serial_instance = serial.serial_for_url(port.decode("utf-8"), baud, do_not_open=True) + + serial_instance.dtr = False + serial_instance.rts = False + + serial_instance.rts = True + serial_instance.open() + serial_instance.rts = False + + # Read until example end and find the currently running partition string + content = serial_instance.read_until(b"Example end") + pattern = re.compile(b"Running partition: ([a-z0-9_]+)") + running = re.search(pattern, content).group(1) + + return running.decode("utf-8") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--port", default=None) + args = parser.parse_args() + + try: + res = get_running_partition(args.port) + except Exception as e: + print(e.message) + sys.exit(1) + + print(res) + + +if __name__ == "__main__": + main() diff --git a/examples/system/ota/otatool/otatool_example.py b/examples/system/ota/otatool/otatool_example.py index 17ed0cdb9e..b2a464eaec 100755 --- a/examples/system/ota/otatool/otatool_example.py +++ b/examples/system/ota/otatool/otatool_example.py @@ -18,20 +18,12 @@ # limitations under the License. import os import sys -import subprocess import argparse -import serial -import re -IDF_PATH = os.path.expandvars("$IDF_PATH") - -OTATOOL_PY = os.path.join(IDF_PATH, "components", "app_update", "otatool.py") -ESPTOOL_PY = os.path.join(IDF_PATH, "components", "esptool_py", "esptool", "esptool.py") - -INVOKE_ARGS = [sys.executable, OTATOOL_PY, "-q"] +from get_running_partition import get_running_partition -def sized_file_compare(file1, file2): +def assert_file_same(file1, file2, err): with open(file1, "rb") as f1: with open(file2, "rb") as f2: f1 = f1.read() @@ -42,122 +34,22 @@ def sized_file_compare(file1, file2): else: f1 = f1[:len(f2)] - return f1 == f2 + if not f1 == f2: + raise Exception(err) -def check(condition, message): - if not condition: - print("Error: " + message) - sys.exit(1) - - -def flash_example_firmware_to_ota_partitions(args): - # Invokes the command - # - # otatool.py -q write_ota_partition --slot or - # otatool.py -q write_ota_partition --name - # - # to write the contents of a file to the specified ota partition (either using name or the slot number) - print("Writing factory firmware to ota_0") - invoke_args = INVOKE_ARGS + ["write_ota_partition", "--slot", "0", "--input", args.binary] - subprocess.check_call(invoke_args) - - print("Writing factory firmware to ota_1") - invoke_args = INVOKE_ARGS + ["write_ota_partition", "--name", "ota_1", "--input", args.binary] - subprocess.check_call(invoke_args) - - # Verify that the contents of the two ota slots are the same as that of the factory partition - print("Checking written firmware to ota_0 and ota_1 match factory firmware") - - # Invokes the command - # - # otatool.py -q read_ota_partition --slot or - # otatool.py -q read_ota_partition --name - # - # to read the contents of a specified ota partition (either using name or the slot number) and write to a file - invoke_args = INVOKE_ARGS + ["read_ota_partition", "--slot", "0", "--output", "app_0.bin"] - subprocess.check_call(invoke_args) - - invoke_args = INVOKE_ARGS + ["read_ota_partition", "--name", "ota_1", "--output", "app_1.bin"] - subprocess.check_call(invoke_args) - - ota_same = sized_file_compare("app_0.bin", args.binary) - check(ota_same, "Slot 0 app does not match factory app") - - ota_same = sized_file_compare("app_1.bin", args.binary) - check(ota_same, "Slot 1 app does not match factory app") - - -def check_running_ota_partition(expected, port=None): - # Monitor the serial output of target device. The firmware outputs the currently - # running partition. It should match the partition the otatool switched to. - - if expected == 0 or expected == "ota_0": - expected = b"ota_0" - elif expected == 1 or expected == "ota_1": - expected = b"ota_1" - else: - expected = b"factory" - - sys.path.append(os.path.join(IDF_PATH, 'components', 'esptool_py', 'esptool')) - import esptool - - baud = os.environ.get("ESPTOOL_BAUD", esptool.ESPLoader.ESP_ROM_BAUD) - - if not port: - # Check what esptool.py finds on what port the device is connected to - output = subprocess.check_output([sys.executable, ESPTOOL_PY, "chip_id"]) - pattern = r"Serial port ([\S]+)" - pattern = re.compile(pattern.encode()) - port = re.search(pattern, output).group(1) - - serial_instance = serial.serial_for_url(port.decode("utf-8"), baud, do_not_open=True) - - serial_instance.dtr = False - serial_instance.rts = False - - serial_instance.rts = True - serial_instance.open() - serial_instance.rts = False - - # Read until example end and find the currently running partition string - content = serial_instance.read_until(b"Example end") - pattern = re.compile(b"Running partition: ([a-z0-9_]+)") - running = re.search(pattern, content).group(1) - - check(expected == running, "Running partition %s does not match expected %s" % (running, expected)) - - -def switch_partition(part, port): - if isinstance(part, int): - spec = "slot" - else: - spec = "name" - - print("Switching to ota partition %s %s" % (spec, str(part))) - - if str(part) == "factory": - # Invokes the command - # - # otatool.py -q erase_otadata - # - # to erase the otadata partition, effectively setting boot firmware to - # factory - subprocess.check_call(INVOKE_ARGS + ["erase_otadata"]) - else: - # Invokes the command - # - # otatool.py -q switch_otadata --slot or - # otatool.py -q switch_otadata --name - # - # to switch to the indicated ota partition (either using name or the slot number) - subprocess.check_call(INVOKE_ARGS + ["switch_otadata", "--" + spec, str(part)]) - - check_running_ota_partition(part, port) +def assert_running_partition(expected, port=None): + running = get_running_partition(port) + if running != expected: + raise Exception("Running partition %s does not match expected %s" % (running, expected)) def main(): - global INVOKE_ARGS + COMPONENTS_PATH = os.path.expandvars(os.path.join("$IDF_PATH", "components")) + OTATOOL_DIR = os.path.join(COMPONENTS_PATH, "app_update") + + sys.path.append(OTATOOL_DIR) + from otatool import OtatoolTarget parser = argparse.ArgumentParser("ESP-IDF OTA Tool Example") @@ -165,29 +57,61 @@ def main(): parser.add_argument("--binary", "-b", help="path to built example binary", default=os.path.join("build", "otatool.bin")) args = parser.parse_args() - if args.port: - INVOKE_ARGS += ["--port", args.port] + target = OtatoolTarget(args.port) - # Flash the factory firmware to all ota partitions - flash_example_firmware_to_ota_partitions(args) + print("Writing factory firmware to ota_0") + target.write_ota_partition(0, args.binary) - # Perform switching ota partitions - switch_partition("factory", args.port) - switch_partition("factory", args.port) # check switching to factory partition twice in a row + print("Writing factory firmware to ota_1") + target.write_ota_partition("ota_1", args.binary) - switch_partition(0, args.port) + # Verify that the contents of the two ota slots are the same as that of the factory partition + print("Checking written firmware to ota_0 and ota_1 match factory firmware") + target.read_ota_partition("ota_0", "app0.bin") + target.read_ota_partition(1, "app1.bin") - switch_partition("ota_1", args.port) - switch_partition(1, args.port) # check switching to ota_1 partition twice in a row + assert_file_same("app0.bin", args.binary, "Slot 0 app does not match factory app") + assert_file_same("app1.bin", args.binary, "Slot 1 app does not match factory app") - switch_partition("ota_0", args.port) - switch_partition(0, args.port) # check switching to ota_0 partition twice in a row + # Switch to factory app + print("Switching to factory app") + target.erase_otadata() + assert_running_partition("factory") - switch_partition("factory", args.port) + # Switch to slot 0 + print("Switching to OTA slot 0") + target.switch_ota_partition(0) + assert_running_partition("ota_0") - switch_partition(1, args.port) # check switching to ota_1 partition from factory + # Switch to slot 1 twice in a row + print("Switching to OTA slot 1 (twice in a row)") + target.switch_ota_partition(1) + assert_running_partition("ota_1") + target.switch_ota_partition("ota_1") + assert_running_partition("ota_1") + # Switch to slot 0 twice in a row + print("Switching to OTA slot 0 (twice in a row)") + target.switch_ota_partition(0) + assert_running_partition("ota_0") + target.switch_ota_partition("ota_0") + assert_running_partition("ota_0") + + # Switch to factory app + print("Switching to factory app") + target.erase_otadata() + assert_running_partition("factory") + + # Switch to slot 1 + print("Switching to OTA slot 1") + target.switch_ota_partition(1) + assert_running_partition("ota_1") + + # Example end and cleanup print("\nOTA tool operations executed successfully!") + clean_files = ["app0.bin", "app1.bin"] + for clean_file in clean_files: + os.unlink(clean_file) if __name__ == '__main__': diff --git a/examples/system/ota/otatool/otatool_example.sh b/examples/system/ota/otatool/otatool_example.sh new file mode 100644 index 0000000000..bcf8456dcc --- /dev/null +++ b/examples/system/ota/otatool/otatool_example.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# +# Demonstrates command-line interface of OTA Partitions Tool, otatool.py +# +# +# $1 - serial port where target device to operate on is connnected to, by default the first found valid serial port +# $2 - path to this example's built binary file (parttool.bin), by default $PWD/build/otatool.bin + +PORT=$1 +OTATOOL_PY="python $IDF_PATH/components/app_update/otatool.py -q" + +if [[ "$PORT" != "" ]]; then + OTATOOL_PY="$OTATOOL_PY --port $PORT" +fi + +BINARY=$2 + +if [[ "$BINARY" == "" ]]; then + BINARY=build/otatool.bin +fi + +function assert_file_same() +{ + sz_a=$(stat -c %s $1) + sz_b=$(stat -c %s $2) + sz=$((sz_a < sz_b ? sz_a : sz_b)) + res=$(cmp -s -n $sz $1 $2) || + (echo "!!!!!!!!!!!!!!!!!!!" + echo "FAILURE: $3" + echo "!!!!!!!!!!!!!!!!!!!") +} + +function assert_running_partition() +{ + running=$(python get_running_partition.py) + if [[ "$running" != "$1" ]]; then + echo "!!!!!!!!!!!!!!!!!!!" + echo "FAILURE: Running partition '$running' does not match expected '$1'" + echo "!!!!!!!!!!!!!!!!!!!" + exit 1 + fi +} + +# Flash the example firmware to OTA partitions. The first write uses slot number to identify OTA +# partition, the second one uses the name. +echo "Writing factory firmware to ota_0" +$OTATOOL_PY write_ota_partition --slot 0 --input $BINARY + +echo "Writing factory firmware to ota_1" +$OTATOOL_PY write_ota_partition --name ota_1 --input $BINARY + +# Read back the written firmware +$OTATOOL_PY read_ota_partition --name ota_0 --output app0.bin +$OTATOOL_PY read_ota_partition --slot 1 --output app1.bin + +assert_file_same $BINARY app0.bin "Slot 0 app does not match factory app" +assert_file_same $BINARY app1.bin "Slot 1 app does not match factory app" + +# Switch to factory app +echo "Switching to factory app" +$OTATOOL_PY erase_otadata +assert_running_partition factory + +# Switch to slot 0 +echo "Switching to OTA slot 0" +$OTATOOL_PY switch_ota_partition --slot 0 +assert_running_partition ota_0 + +# Switch to slot 1 twice in a row +echo "Switching to OTA slot 1 (twice in a row)" +$OTATOOL_PY switch_ota_partition --slot 1 +assert_running_partition ota_1 +$OTATOOL_PY switch_ota_partition --name ota_1 +assert_running_partition ota_1 + +# Switch to slot 0 twice in a row +echo "Switching to OTA slot 0 (twice in a row)" +$OTATOOL_PY switch_ota_partition --slot 0 +assert_running_partition ota_0 +$OTATOOL_PY switch_ota_partition --name ota_0 +assert_running_partition ota_0 + +# Switch to factory app +echo "Switching to factory app" +$OTATOOL_PY erase_otadata +assert_running_partition factory + +# Switch to slot 1 +echo "Switching to OTA slot 1" +$OTATOOL_PY switch_ota_partition --slot 1 +assert_running_partition ota_1 + +# Example end and cleanup +printf "\nPartition tool operations performed successfully\n" +rm -rf app0.bin app1.bin \ No newline at end of file diff --git a/tools/ci/executable-list.txt b/tools/ci/executable-list.txt index f6ac3d1e7a..e0637d9bf8 100644 --- a/tools/ci/executable-list.txt +++ b/tools/ci/executable-list.txt @@ -23,7 +23,9 @@ examples/build_system/cmake/idf_as_lib/run-esp32.sh examples/build_system/cmake/idf_as_lib/run.sh examples/storage/parttool/parttool_example.py examples/storage/parttool/parttool_example.sh +examples/system/ota/otatool/get_running_partition.py examples/system/ota/otatool/otatool_example.py +examples/system/ota/otatool/otatool_example.sh tools/check_kconfigs.py tools/check_python_dependencies.py tools/ci/apply_bot_filter.py