From 2aaab7eeefbbd1104cd8dd1e48d9a0348f564cb0 Mon Sep 17 00:00:00 2001 From: Ivan Grokhotkov Date: Thu, 2 Apr 2020 03:32:47 +0200 Subject: [PATCH 1/2] tools: add script to generate DFU binaries for ESP32-S2 --- tools/mkdfu.py | 212 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100755 tools/mkdfu.py diff --git a/tools/mkdfu.py b/tools/mkdfu.py new file mode 100755 index 0000000000..eecd00b550 --- /dev/null +++ b/tools/mkdfu.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# +# This program creates archives compatible with ESP32-S* ROM DFU implementation. +# +# The archives are in CPIO format. Each file which needs to be flashed is added to the archive +# as a separate file. In addition to that, a special index file, 'dfuinfo0.dat', is created. +# This file must be the first one in the archive. It contains binary structures describing each +# subsequent file (for example, where the file needs to be flashed/loaded). +# + +import argparse +from collections import namedtuple +import hashlib +import os +import struct +import zlib + +try: + import typing +except ImportError: + # Only used for type annotations + pass + +# CPIO ("new ASCII") format related things +CPIO_MAGIC = b"070701" +CPIO_STRUCT = b"=6s" + b"8s" * 13 +CPIOHeader = namedtuple( + "CPIOHeader", + [ + "magic", + "ino", + "mode", + "uid", + "gid", + "nlink", + "mtime", + "filesize", + "devmajor", + "devminor", + "rdevmajor", + "rdevminor", + "namesize", + "check", + ], +) +CPIO_TRAILER = "TRAILER!!!" + + +def make_cpio_header( + filename_len, file_len, is_trailer=False +): # type: (int, int, bool) -> CPIOHeader + """ Returns CPIOHeader for the given file name and file size """ + + def as_hex(val): # type: (int) -> bytes + return "{:08x}".format(val).encode("ascii") + + hex_0 = as_hex(0) + mode = hex_0 if is_trailer else as_hex(0o0100644) + nlink = as_hex(1) if is_trailer else hex_0 + return CPIOHeader( + magic=CPIO_MAGIC, + ino=hex_0, + mode=mode, + uid=hex_0, + gid=hex_0, + nlink=nlink, + mtime=hex_0, + filesize=as_hex(file_len), + devmajor=hex_0, + devminor=hex_0, + rdevmajor=hex_0, + rdevminor=hex_0, + namesize=as_hex(filename_len), + check=hex_0, + ) + + +# DFU format related things +# Structure of one entry in dfuinfo0.dat +DFUINFO_STRUCT = b" int + """ Calculate CRC32/JAMCRC of data, with an optional initial value """ + uint32_max = 0xFFFFFFFF + return uint32_max - (zlib.crc32(data, crc) & uint32_max) + + +def pad_bytes(b, multiple, padding=b"\x00"): # type: (bytes, int, bytes) -> bytes + """ Pad 'b' to a length divisible by 'multiple' """ + padded_len = (len(b) + multiple - 1) // multiple * multiple + return b + padding * (padded_len - len(b)) + + +class EspDfuWriter(object): + def __init__(self, dest_file): # type: (typing.BinaryIO) -> None + self.dest = dest_file + self.entries = [] # type: typing.List[bytes] + self.index = [] # type: typing.List[DFUInfo] + + def add_file(self, flash_addr, path): # type: (int, str) -> None + """ Add file to be written into flash at given address """ + with open(path, "rb") as f: + self._add_cpio_flash_entry(os.path.basename(path), flash_addr, f.read()) + + def finish(self): # type: () -> None + """ Write DFU file """ + # Prepare and add dfuinfo0.dat file + dfuinfo = b"".join([struct.pack(DFUINFO_STRUCT, *item) for item in self.index]) + self._add_cpio_entry(DFUINFO_FILE, dfuinfo, first=True) + + # Add CPIO archive trailer + self._add_cpio_entry(CPIO_TRAILER, b"", trailer=True) + + # Combine all the entries and pad the file + out_data = b"".join(self.entries) + cpio_block_size = 10240 + out_data = pad_bytes(out_data, cpio_block_size) + + # Add DFU suffix and CRC + out_data += struct.pack(DFUSUFFIX_STRUCT, *DFUSUFFIX_DEFAULT) + out_data += struct.pack(DFUCRC_STRUCT, dfu_crc(out_data)) + + # Finally write the entire binary + self.dest.write(out_data) + + def _add_cpio_flash_entry( + self, filename, flash_addr, data + ): # type: (str, int, bytes) -> None + md5 = hashlib.md5() + md5.update(data) + self.index.append( + DFUInfo( + address=flash_addr, + flags=0, + name=filename.encode("utf-8"), + md5=md5.digest(), + ) + ) + self._add_cpio_entry(filename, data) + + def _add_cpio_entry( + self, filename, data, first=False, trailer=False + ): # type: (str, bytes, bool, bool) -> None + filename_b = filename.encode("utf-8") + b"\x00" + cpio_header = make_cpio_header(len(filename_b), len(data), is_trailer=trailer) + entry = pad_bytes( + struct.pack(CPIO_STRUCT, *cpio_header) + filename_b, 4 + ) + pad_bytes(data, 4) + if not first: + self.entries.append(entry) + else: + self.entries.insert(0, entry) + + +def action_write(args): + writer = EspDfuWriter(args.output_file) + for addr, file in args.files: + writer.add_file(addr, file) + writer.finish() + + +class WriteArgsAction(argparse.Action): + """ Helper for argparse to parse argument pairs """ + + def __init__(self, *args, **kwargs): + super(WriteArgsAction, self).__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + # TODO: add validation + addr = 0 + result = [] + for i, value in enumerate(values): + if i % 2 == 0: + addr = int(value, 0) + else: + result.append((addr, value)) + + setattr(namespace, self.dest, result) + + +def main(): + parser = argparse.ArgumentParser("mkdfu") + + # Provision to add "info" command + subparsers = parser.add_subparsers(dest="command") + write_parser = subparsers.add_parser("write") + write_parser.add_argument("-o", "--output-file", type=argparse.FileType("wb")) + write_parser.add_argument( + "files", metavar="
", action=WriteArgsAction, nargs="+" + ) + + args = parser.parse_args() + print(repr(args)) + if args.command == "write": + action_write(args) + + +if __name__ == "__main__": + main() From ab9f7142486aeb7d6e991354da0693caa6421c69 Mon Sep 17 00:00:00 2001 From: Roland Dobai Date: Mon, 6 Apr 2020 16:41:44 +0200 Subject: [PATCH 2/2] Add build system support for programming ESP32-S2 using DFU utils --- docs/conf_common.py | 1 + docs/en/api-guides/dfu.rst | 99 +++++++++++++++++ docs/en/api-guides/index.rst | 1 + docs/en/api-guides/tools/idf-tools-notes.inc | 5 + docs/en/get-started/linux-setup-scratch.rst | 6 +- docs/en/get-started/linux-setup.rst | 6 +- docs/en/get-started/macos-setup-scratch.rst | 4 +- docs/en/get-started/macos-setup.rst | 4 +- docs/zh_CN/api-guides/dfu.rst | 1 + docs/zh_CN/api-guides/index.rst | 1 + .../api-guides/tools/idf-tools-notes.inc | 5 + .../zh_CN/get-started/linux-setup-scratch.rst | 6 +- docs/zh_CN/get-started/linux-setup.rst | 6 +- .../zh_CN/get-started/macos-setup-scratch.rst | 4 +- docs/zh_CN/get-started/macos-setup.rst | 4 +- tools/ci/config/host-test.yml | 8 ++ tools/ci/executable-list.txt | 2 + tools/ci/test_build_system_cmake.sh | 10 ++ tools/cmake/dfu.cmake | 26 +++++ tools/cmake/idf.cmake | 1 + tools/cmake/project.cmake | 3 + tools/idf.py | 2 +- tools/idf_py_actions/dfu_ext.py | 39 +++++++ tools/idf_py_actions/tools.py | 7 ++ tools/mkdfu.py | 103 ++++++++++++------ tools/test_mkdfu/1/1.bin | Bin 0 -> 1024 bytes tools/test_mkdfu/1/2.bin | Bin 0 -> 2048 bytes tools/test_mkdfu/1/3.bin | Bin 0 -> 3072 bytes tools/test_mkdfu/1/dfu.bin | Bin 0 -> 10256 bytes tools/test_mkdfu/1/flasher_args.json | 7 ++ tools/test_mkdfu/test_mkdfu.py | 99 +++++++++++++++++ tools/tools.json | 37 +++++++ 32 files changed, 444 insertions(+), 53 deletions(-) create mode 100644 docs/en/api-guides/dfu.rst create mode 100644 docs/zh_CN/api-guides/dfu.rst create mode 100644 tools/cmake/dfu.cmake create mode 100644 tools/idf_py_actions/dfu_ext.py create mode 100644 tools/test_mkdfu/1/1.bin create mode 100644 tools/test_mkdfu/1/2.bin create mode 100644 tools/test_mkdfu/1/3.bin create mode 100644 tools/test_mkdfu/1/dfu.bin create mode 100644 tools/test_mkdfu/1/flasher_args.json create mode 100755 tools/test_mkdfu/test_mkdfu.py diff --git a/docs/conf_common.py b/docs/conf_common.py index 36ecf4ff75..e7ad6e2dd2 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -159,6 +159,7 @@ def update_exclude_patterns(tags): # note: in toctrees, these also need to be marked with a :esp32: filter for e in ['esp32s2.rst', 'hw-reference/esp32s2/**', + 'api-guides/dfu.rst', 'api-guides/ulps2_instruction_set.rst', 'api-reference/peripherals/hmac.rst', 'api-reference/peripherals/temp_sensor.rst']: diff --git a/docs/en/api-guides/dfu.rst b/docs/en/api-guides/dfu.rst new file mode 100644 index 0000000000..251b58c5c0 --- /dev/null +++ b/docs/en/api-guides/dfu.rst @@ -0,0 +1,99 @@ +*********************************************** +Device Firmware Upgrade through USB +*********************************************** + +.. only:: esp32 + + .. note:: + Device Firmware Upgrade through USB is not supported with ESP32 chips. + +Device Firmware Upgrade (DFU) is a mechanism for upgrading the firmware of devices through Universal Serial Bus (USB). +DFU is supported by ESP32-S2 chips. The necessary connections for the USB peripheral are shown in the following table. + ++------+-------------+ +| GPIO | USB | ++======+=============+ +| 19 | D- (green) | ++------+-------------+ +| 20 | D+ (white) | ++------+-------------+ +| GND | GND (black) | ++------+-------------+ +| | +5V (red) | ++------+-------------+ + +The software requirements of DFU are included in :ref:`get-started-get-prerequisites` of the Getting Started Guide. + +Section :ref:`api_guide_dfu_build` describes how to build firmware for DFU with ESP-IDF and +Section :ref:`api_guide_dfu_flash` deals with flashing the firmware. + +.. _api_guide_dfu_build: + +Building the DFU Image +====================== + +The DFU image can be created by running:: + + idf.py dfu + +which creates ``dfu.bin`` in the build directory. + +.. note:: + Don't forget to set the target chip by ``idf.py set-target`` before running ``idf.py dfu``. Otherwise, you might + create an image for a different chip or receive an error message something like ``unknown target 'dfu'``. + +.. _api_guide_dfu_flash: + +Flashing the Chip with the DFU Image +==================================== + +The DFU image is downloaded into the chip by running:: + + idf.py dfu-flash + +which relies on `dfu-util `_. Please see :ref:`get-started-get-prerequisites` for +installing ``dfu-util``. ``dfu-util`` needs additional setup for :ref:`api_guide_dfu_flash_win` or setting up an +:ref:`api_guide_dfu_flash_udev`. Mac OS users should be able to use ``dfu-util`` without further setup. + +See :ref:`api_guide_dfu_flash_errors` and their solutions. + +.. _api_guide_dfu_flash_udev: + +udev rule (Linux only) +---------------------- + +udev is a device manager for the Linux kernel. It allows us to run ``dfu-util`` (and ``idf.py dfu-flash``) without +``sudo`` for gaining access to the chip. + +Create file ``/etc/udev/rules.d/40-dfuse.rules`` with the following content:: + + SUBSYSTEMS=="usb", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="00??", GROUP="plugdev", MODE="0666" + +.. note:: + Please check the output of command ``groups``. The user has to be a member of the `GROUP` specified above. You may + use some other existing group for this purpose (e.g. `uucp` on some systems instead of `plugdev`) or create a new + group for this purpose. + +Restart your computer so the previous setting could take into affect or run ``sudo udevadm trigger`` to force +manually udev to trigger your new rule. + +.. _api_guide_dfu_flash_win: + +USB drivers (Windows only) +-------------------------- + +``dfu-util`` uses `libusb` to access the device. You have to register on Windows the device with the `WinUSB` driver. +Please see the `libusb wiki `_ for more +details. + +.. _api_guide_dfu_flash_errors: + +Common errors +------------- + +- ``dfu-util: command not found`` might indicate that the tool hasn't been installed or is not available from the terminal. + An easy way of checking the tool is running ``dfu-util --version``. Please see :ref:`get-started-get-prerequisites` for + installing ``dfu-util``. +- The reason for ``No DFU capable USB device available`` could be that the USB driver wasn't properly installed on + Windows (see :ref:`api_guide_dfu_flash_win`) or udev rule was not setup on Linux + (see :ref:`api_guide_dfu_flash_udev`). diff --git a/docs/en/api-guides/index.rst b/docs/en/api-guides/index.rst index 4b11122acd..946b85f6c5 100644 --- a/docs/en/api-guides/index.rst +++ b/docs/en/api-guides/index.rst @@ -11,6 +11,7 @@ API Guides Build System :esp32: Build System (Legacy GNU Make) Deep Sleep Wake Stubs + :esp32s2: Device Firmware Upgrade through USB Error Handling :esp32: ESP-BLE-MESH ESP-MESH (Wi-Fi) diff --git a/docs/en/api-guides/tools/idf-tools-notes.inc b/docs/en/api-guides/tools/idf-tools-notes.inc index ffd58cd364..426b33edbc 100644 --- a/docs/en/api-guides/tools/idf-tools-notes.inc +++ b/docs/en/api-guides/tools/idf-tools-notes.inc @@ -47,4 +47,9 @@ On Linux and macOS, it is recommended to install ninja using the OS-specific pac .. tool-ccache-notes +--- + +.. tool-dfu-util-notes + + --- diff --git a/docs/en/get-started/linux-setup-scratch.rst b/docs/en/get-started/linux-setup-scratch.rst index 1ba2f560df..07b236c9ac 100644 --- a/docs/en/get-started/linux-setup-scratch.rst +++ b/docs/en/get-started/linux-setup-scratch.rst @@ -15,15 +15,15 @@ To compile with ESP-IDF you need to get the following packages: - CentOS 7:: - sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache + sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache dfu-util - Ubuntu and Debian:: - sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache + sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache dfu-util .. note:: CMake version 3.5 or newer is required for use with ESP-IDF. Older Linux distributions may require updating, enabling of a "backports" repository, or installing of a "cmake3" package rather than "cmake". diff --git a/docs/en/get-started/linux-setup.rst b/docs/en/get-started/linux-setup.rst index bde40fe2af..80fcc75b1d 100644 --- a/docs/en/get-started/linux-setup.rst +++ b/docs/en/get-started/linux-setup.rst @@ -11,15 +11,15 @@ To compile with ESP-IDF you need to get the following packages: - CentOS 7:: - sudo yum install git wget flex bison gperf python cmake ninja-build ccache + sudo yum install git wget flex bison gperf python cmake ninja-build ccache dfu-util - Ubuntu and Debian:: - sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make flex bison gperf python-pip cmake ninja ccache + sudo pacman -S --needed gcc git make flex bison gperf python-pip cmake ninja ccache dfu-util .. note:: CMake version 3.5 or newer is required for use with ESP-IDF. Older Linux distributions may require updating, enabling of a "backports" repository, or installing of a "cmake3" package rather than "cmake". diff --git a/docs/en/get-started/macos-setup-scratch.rst b/docs/en/get-started/macos-setup-scratch.rst index e906b9fe78..1223730384 100644 --- a/docs/en/get-started/macos-setup-scratch.rst +++ b/docs/en/get-started/macos-setup-scratch.rst @@ -31,11 +31,11 @@ Install Prerequisites - If you have HomeBrew, you can run:: - brew install cmake ninja + brew install cmake ninja dfu-util - If you have MacPorts, you can run:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util Compile the Toolchain from Source ================================= diff --git a/docs/en/get-started/macos-setup.rst b/docs/en/get-started/macos-setup.rst index caede42ece..5a072c3d73 100644 --- a/docs/en/get-started/macos-setup.rst +++ b/docs/en/get-started/macos-setup.rst @@ -21,11 +21,11 @@ ESP-IDF will use the version of Python installed by default on macOS. - If you have HomeBrew_, you can run:: - brew install cmake ninja + brew install cmake ninja dfu-util - If you have MacPorts_, you can run:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util - Otherwise, consult the CMake_ and Ninja_ home pages for macOS installation downloads. diff --git a/docs/zh_CN/api-guides/dfu.rst b/docs/zh_CN/api-guides/dfu.rst new file mode 100644 index 0000000000..566cdee390 --- /dev/null +++ b/docs/zh_CN/api-guides/dfu.rst @@ -0,0 +1 @@ +.. include:: ../../en/api-guides/dfu.rst diff --git a/docs/zh_CN/api-guides/index.rst b/docs/zh_CN/api-guides/index.rst index 09a32d945d..9a08e2ad3c 100644 --- a/docs/zh_CN/api-guides/index.rst +++ b/docs/zh_CN/api-guides/index.rst @@ -12,6 +12,7 @@ API 指南 严重错误 Event Handling Deep Sleep Wake Stubs + :esp32s2: Device Firmware Upgrade through USB ESP32 Core Dump Flash Encryption <../security/flash-encryption> FreeRTOS SMP Changes diff --git a/docs/zh_CN/api-guides/tools/idf-tools-notes.inc b/docs/zh_CN/api-guides/tools/idf-tools-notes.inc index c7c750089e..946e8222cd 100644 --- a/docs/zh_CN/api-guides/tools/idf-tools-notes.inc +++ b/docs/zh_CN/api-guides/tools/idf-tools-notes.inc @@ -49,4 +49,9 @@ On Linux and macOS, it is recommended to install ninja using the OS package mana .. tool-ccache-notes +--- + +.. tool-dfu-util-notes + + --- diff --git a/docs/zh_CN/get-started/linux-setup-scratch.rst b/docs/zh_CN/get-started/linux-setup-scratch.rst index 3d8bee637e..5964e4c3ca 100644 --- a/docs/zh_CN/get-started/linux-setup-scratch.rst +++ b/docs/zh_CN/get-started/linux-setup-scratch.rst @@ -13,15 +13,15 @@ - CentOS 7:: - sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache + sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache dfu-util - Ubuntu 和 Debian:: - sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache + sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache dfu-util .. note:: 使用 ESP-IDF 需要 CMake 3.5 或以上版本。较早版本的 Linux 可能需要升级才能向后移植仓库,或安装 "cmake3" 软件包,而不是安装 "cmake"。 diff --git a/docs/zh_CN/get-started/linux-setup.rst b/docs/zh_CN/get-started/linux-setup.rst index 4f6019857d..38b04a8552 100644 --- a/docs/zh_CN/get-started/linux-setup.rst +++ b/docs/zh_CN/get-started/linux-setup.rst @@ -11,15 +11,15 @@ Linux 平台工具链的标准设置 - CentOS 7:: - sudo yum install git wget flex bison gperf python cmake ninja-build ccache + sudo yum install git wget flex bison gperf python cmake ninja-build ccache dfu-util - Ubuntu 和 Debian:: - sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make flex bison gperf python-pip python-pyserial cmake ninja ccache + sudo pacman -S --needed gcc git make flex bison gperf python-pip python-pyserial cmake ninja ccache dfu-util .. note:: 使用 ESP-IDF 需要 CMake 3.5 或以上版本。较早版本的 Linux 可能需要升级才能向后移植仓库,或安装 "cmake3" 软件包,而不是安装 "cmake"。 diff --git a/docs/zh_CN/get-started/macos-setup-scratch.rst b/docs/zh_CN/get-started/macos-setup-scratch.rst index f880317cd9..68c0d5abab 100644 --- a/docs/zh_CN/get-started/macos-setup-scratch.rst +++ b/docs/zh_CN/get-started/macos-setup-scratch.rst @@ -31,11 +31,11 @@ MacPorts 需要完整的 XCode 软件,而 homebrew 只需要安装 XCode 命 - 若有 HomeBrew,您可以运行:: - brew install cmake ninja + brew install cmake ninja dfu-util - 若有 MacPorts,您可以运行:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util 从源代码编译工具链 ================================= diff --git a/docs/zh_CN/get-started/macos-setup.rst b/docs/zh_CN/get-started/macos-setup.rst index 4e9322550b..c5d3e52bd1 100644 --- a/docs/zh_CN/get-started/macos-setup.rst +++ b/docs/zh_CN/get-started/macos-setup.rst @@ -21,11 +21,11 @@ ESP-IDF 将使用 Mac OS 上默认安装的 Python 版本。 - 若有 HomeBrew_,您可以运行:: - brew install cmake ninja + brew install cmake ninja dfu-util - 若有 MacPorts_,您可以运行:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util - 若以上均不适用,请访问 CMake_ 和 Ninja_ 主页,查询有关 Mac OS 平台的下载安装问题。 diff --git a/tools/ci/config/host-test.yml b/tools/ci/config/host-test.yml index 84ec1cee81..e7018ddc1b 100644 --- a/tools/ci/config/host-test.yml +++ b/tools/ci/config/host-test.yml @@ -304,3 +304,11 @@ test_sysviewtrace_proc: script: - cd ${IDF_PATH}/tools/esp_app_trace/test/sysview - ${IDF_PATH}/tools/ci/multirun_with_pyenv.sh ./test.sh + +test_mkdfu: + extends: .host_test_template + variables: + LC_ALL: C.UTF-8 + script: + - cd ${IDF_PATH}/tools/test_mkdfu + - ${IDF_PATH}/tools/ci/multirun_with_pyenv.sh ./test_mkdfu.py diff --git a/tools/ci/executable-list.txt b/tools/ci/executable-list.txt index 690ba367fe..426e308513 100644 --- a/tools/ci/executable-list.txt +++ b/tools/ci/executable-list.txt @@ -85,12 +85,14 @@ tools/ldgen/ldgen.py tools/ldgen/test/test_fragments.py tools/ldgen/test/test_generation.py tools/mass_mfg/mfg_gen.py +tools/mkdfu.py tools/set-submodules-to-github.sh tools/test_check_kconfigs.py tools/test_idf_monitor/run_test_idf_monitor.py tools/test_idf_py/test_idf_py.py tools/test_idf_size/test.sh tools/test_idf_tools/test_idf_tools.py +tools/test_mkdfu/test_mkdfu.py tools/unit-test-app/tools/get_available_configs.sh tools/unit-test-app/unit_test.py tools/windows/eclipse_make.sh diff --git a/tools/ci/test_build_system_cmake.sh b/tools/ci/test_build_system_cmake.sh index ad7e8e49ba..5552344681 100755 --- a/tools/ci/test_build_system_cmake.sh +++ b/tools/ci/test_build_system_cmake.sh @@ -700,6 +700,16 @@ endmenu\n" >> ${IDF_PATH}/Kconfig bin_header_match build/bootloader/bootloader.bin "021f" rm sdkconfig + print_status "DFU build works" + rm -f -r build sdkconfig + idf.py dfu &> tmp.log + grep "command \"dfu\" is not known to idf.py and is not a Ninja target" tmp.log || (tail -n 100 tmp.log ; failure "DFU build should fail for default chip target") + idf.py set-target esp32s2 + idf.py dfu &> tmp.log + grep "build/dfu.bin\" has been written. You may proceed with DFU flashing." tmp.log || (tail -n 100 tmp.log ; failure "DFU build should succeed for esp32s2") + rm tmp.log + assert_built ${APP_BINS} ${BOOTLOADER_BINS} ${PARTITION_BIN} "dfu.bin" + print_status "All tests completed" if [ -n "${FAILURES}" ]; then echo "Some failures were detected:" diff --git a/tools/cmake/dfu.cmake b/tools/cmake/dfu.cmake new file mode 100644 index 0000000000..f29de4b584 --- /dev/null +++ b/tools/cmake/dfu.cmake @@ -0,0 +1,26 @@ +# Add DFU build and flashing related targets +# + +function(__add_dfu_targets) + idf_build_get_property(target IDF_TARGET) + if(NOT "${target}" STREQUAL "esp32s2") + return() + endif() + + idf_build_get_property(python PYTHON) + idf_build_get_property(idf_path IDF_PATH) + + add_custom_target(dfu + COMMAND ${python} ${idf_path}/tools/mkdfu.py write + -o "${CMAKE_CURRENT_BINARY_DIR}/dfu.bin" + --json "${CMAKE_CURRENT_BINARY_DIR}/flasher_args.json" + DEPENDS gen_project_binary bootloader + VERBATIM + USES_TERMINAL) + + add_custom_target(dfu-flash + COMMAND dfu-util + -D "${CMAKE_CURRENT_BINARY_DIR}/dfu.bin" + VERBATIM + USES_TERMINAL) +endfunction() diff --git a/tools/cmake/idf.cmake b/tools/cmake/idf.cmake index 2d396c8863..82468355e9 100644 --- a/tools/cmake/idf.cmake +++ b/tools/cmake/idf.cmake @@ -43,6 +43,7 @@ if(NOT __idf_env_set) include(utilities) include(targets) include(ldgen) + include(dfu) include(version) __build_init("${idf_path}") diff --git a/tools/cmake/project.cmake b/tools/cmake/project.cmake index 11b6c79166..2ec9ea4a6e 100644 --- a/tools/cmake/project.cmake +++ b/tools/cmake/project.cmake @@ -486,6 +486,9 @@ macro(project project_name) unset(idf_size) + # Add DFU build and flash targets + __add_dfu_targets() + idf_build_executable(${project_elf}) __project_info("${test_components}") diff --git a/tools/idf.py b/tools/idf.py index 39716210d0..118ff5c306 100755 --- a/tools/idf.py +++ b/tools/idf.py @@ -447,7 +447,7 @@ def init_cli(verbose_output=None): def _print_closing_message(self, args, actions): # print a closing message of some kind # - if "flash" in str(actions): + if "flash" in str(actions) or "dfu" in str(actions): print("Done") return diff --git a/tools/idf_py_actions/dfu_ext.py b/tools/idf_py_actions/dfu_ext.py new file mode 100644 index 0000000000..af8d2d7fa0 --- /dev/null +++ b/tools/idf_py_actions/dfu_ext.py @@ -0,0 +1,39 @@ +from idf_py_actions.tools import is_target_supported, ensure_build_directory, run_target +from idf_py_actions.errors import FatalError + + +def action_extensions(base_actions, project_path): + + SUPPORTED_TARGETS = ['esp32s2'] + + def dfu_target(target_name, ctx, args): + ensure_build_directory(args, ctx.info_name) + run_target(target_name, args) + + def dfu_flash_target(target_name, ctx, args): + ensure_build_directory(args, ctx.info_name) + + try: + run_target(target_name, args) + except FatalError: + # Cannot capture the error from dfu-util here so the best advise is: + print('Please have a look at the "Device Firmware Upgrade through USB" chapter in API Guides of the ' + 'ESP-IDF documentation for solving common dfu-util issues.') + raise + + dfu_actions = { + "actions": { + "dfu": { + "callback": dfu_target, + "short_help": "Build the DFU binary", + "dependencies": ["all"], + }, + "dfu-flash": { + "callback": dfu_flash_target, + "short_help": "Flash the DFU binary", + "order_dependencies": ["dfu"], + }, + } + } + + return dfu_actions if is_target_supported(project_path, SUPPORTED_TARGETS) else {} diff --git a/tools/idf_py_actions/tools.py b/tools/idf_py_actions/tools.py index 663522b82c..d96ee9f35c 100644 --- a/tools/idf_py_actions/tools.py +++ b/tools/idf_py_actions/tools.py @@ -269,6 +269,13 @@ def get_sdkconfig_value(sdkconfig_file, key): return value +def is_target_supported(project_path, supported_targets): + """ + Returns True if the active target is supported, or False otherwise. + """ + return get_sdkconfig_value(os.path.join(project_path, "sdkconfig"), 'CONFIG_IDF_TARGET') in supported_targets + + def _guess_or_check_idf_target(args, prog_name, cache): """ If CMakeCache.txt doesn't exist, and IDF_TARGET is not set in the environment, guess the value from diff --git a/tools/mkdfu.py b/tools/mkdfu.py index eecd00b550..d2e562c717 100755 --- a/tools/mkdfu.py +++ b/tools/mkdfu.py @@ -1,16 +1,31 @@ #!/usr/bin/env python # +# Copyright 2020 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. +# # This program creates archives compatible with ESP32-S* ROM DFU implementation. # # The archives are in CPIO format. Each file which needs to be flashed is added to the archive # as a separate file. In addition to that, a special index file, 'dfuinfo0.dat', is created. # This file must be the first one in the archive. It contains binary structures describing each # subsequent file (for example, where the file needs to be flashed/loaded). -# -import argparse from collections import namedtuple +from future.utils import iteritems +import argparse import hashlib +import json import os import struct import zlib @@ -21,6 +36,12 @@ except ImportError: # Only used for type annotations pass +try: + from itertools import izip as zip +except ImportError: + # Python 3 + pass + # CPIO ("new ASCII") format related things CPIO_MAGIC = b"070701" CPIO_STRUCT = b"=6s" + b"8s" * 13 @@ -166,46 +187,64 @@ class EspDfuWriter(object): def action_write(args): - writer = EspDfuWriter(args.output_file) - for addr, file in args.files: - writer.add_file(addr, file) + writer = EspDfuWriter(args['output_file']) + for addr, f in args['files']: + print('Adding {} at {:#x}'.format(f, addr)) + writer.add_file(addr, f) writer.finish() - - -class WriteArgsAction(argparse.Action): - """ Helper for argparse to parse argument pairs """ - - def __init__(self, *args, **kwargs): - super(WriteArgsAction, self).__init__(*args, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - # TODO: add validation - addr = 0 - result = [] - for i, value in enumerate(values): - if i % 2 == 0: - addr = int(value, 0) - else: - result.append((addr, value)) - - setattr(namespace, self.dest, result) + print('"{}" has been written. You may proceed with DFU flashing.'.format(args['output_file'].name)) def main(): - parser = argparse.ArgumentParser("mkdfu") + parser = argparse.ArgumentParser() # Provision to add "info" command subparsers = parser.add_subparsers(dest="command") write_parser = subparsers.add_parser("write") - write_parser.add_argument("-o", "--output-file", type=argparse.FileType("wb")) - write_parser.add_argument( - "files", metavar="
", action=WriteArgsAction, nargs="+" - ) + write_parser.add_argument("-o", "--output-file", + help='Filename for storing the output DFU image', + required=True, + type=argparse.FileType("wb")) + write_parser.add_argument("--json", + help='Optional file for loading "flash_files" dictionary with
items') + write_parser.add_argument("files", + metavar="
", help='Add at
', + nargs="*") args = parser.parse_args() - print(repr(args)) - if args.command == "write": - action_write(args) + + def check_file(file_name): + if not os.path.isfile(file_name): + raise RuntimeError('{} is not a regular file!'.format(file_name)) + return file_name + + files = [] + if args.files: + files += [(int(addr, 0), check_file(f_name)) for addr, f_name in zip(args.files[::2], args.files[1::2])] + + if args.json: + json_dir = os.path.dirname(os.path.abspath(args.json)) + + def process_json_file(path): + ''' + The input path is relative to json_dir. This function makes it relative to the current working + directory. + ''' + return check_file(os.path.relpath(os.path.join(json_dir, path), start=os.curdir)) + + with open(args.json) as f: + files += [(int(addr, 0), + process_json_file(f_name)) for addr, f_name in iteritems(json.load(f)['flash_files'])] + + files = sorted([(addr, f_name) for addr, f_name in iteritems(dict(files))], + key=lambda x: x[0]) # remove possible duplicates and sort based on the address + + cmd_args = {'output_file': args.output_file, + 'files': files, + } + + {'write': action_write + }[args.command](cmd_args) if __name__ == "__main__": diff --git a/tools/test_mkdfu/1/1.bin b/tools/test_mkdfu/1/1.bin new file mode 100644 index 0000000000000000000000000000000000000000..60320aa0dfc237aa20e77095a77bc7ba1d92c23e GIT binary patch literal 1024 zcmeyi^g<)2%~{<|QDqZ@%(`c|b^ggJH;S5{7JJ0OTKD+f#>l`oohv3Po1|%G?&uIM z`VtE>3vvsXMv%FqctNZYTr=!V`L~`jiT4R?|zManH+0~jA%yOHK*|i(by53)PdE&c9qYtG&tI}>Z z9!PnxM(V^v$NAc`!&B=$(pNZa`0BSasHNyuqV0qlp;ePFZG2VdSF=)u;U{DBKh4Xl zkKXa{au1!oydmLg=US2O5SLx&Kv#a3;!6cWgSYDMgqrOCx`!V5V<%_{i3E= zccYxa>N|nn-m89HIrhM=DvxL7R2T8BtNB{{RvzW_S@E-Wql?wP@(B&KQrqqydC070 zy5nQqRQ&~KZZsGb1g=+a)Z2MDV~dYK%buz8-&mjM4_d3h{blkz+wU@*g$3G2w1QUk z$7?b!bCZ;rEBtu&!imiLy)t%fD2v_1dS-I3@*Nr5(uo;Jn_4Z2dk{NdO=ac8F2QG~aD`&cxwJmV5_8%3l+d5j2*P=gt zGn#oht8n+N9WMk_vWhoePWXG%_OhX-%Gv)u literal 0 HcmV?d00001 diff --git a/tools/test_mkdfu/1/2.bin b/tools/test_mkdfu/1/2.bin new file mode 100644 index 0000000000000000000000000000000000000000..1b9950f09d21d0a83b8d0079da0fd3f56a8b483f GIT binary patch literal 2048 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 UbT$2C@7fl!aQU{PLq37 zfm!4c-&~IGTsiOV+G)~rn<5@0u`)D3%rTQxc}~$E8LH6vP2>F$D^KGY|p7kkJ0N^g<)2%~{<|QDqZ@%(`c| zb^ggJH;S5{7JJ0OTKD+f#>l`oohv3Po1|%G?&uIM`VtE>3vvsXMv%FqctNZYT zr=!V`L~`jiT4R?| zzManH+0~jA%yOHK*|i(by53)PdE&c9qYtG&tI}>Z9!PnxM(V^v$NAc`!&B=$(pNZa z`0BSasHNyuqV0qlp;ePFZG2VdSF=)u;U{DBKh4XlkKXa{au1!oydmLg=US2O5SLx&Kv#a3;!6cWgSYDMgqrOCx`!V5V<%_{i3E=ccYxa>N|nn-m89HIrhM=DvxL7 zR2T8BtNB{{RvzW_S@E-Wql?wP@(B&KQrqqydC070y5nQqRQ&~KZZsGb1g=+a)Z2MD zV~dYK%buz8-&mjM4_d3h{blkz+wU@*g$3G2w1QUk$7?b!bCZ;rEBtu&!imiLy)t%f zD2v_1dS-I3@*Nr5(uo;Jn_4Z2d zk{NdO=ac8F2QG~aD`&cxwJmV5_8%3l+d5j2*P=gtGn#oht8n+N9WMk_vWhoePWXG% z_OhY6lsUwf`UVE@G6zx;CuP;|oUVS^>f7J-sjH*I zPnT3Y>?`Lywx_Li&VjNebI;88lGTpd{>I{&FT$%pySeu9XeDB@s9y({R z{+#a={7GeBvKMEB8-tNU!s^!WRQZ!@=MndVIlCc{={O(he3%3LfRe{yX(#f zusTlqu%}JR)8Whu&C5HZdcTAuJbIp#@W#GHEOA?;#;rq@3xD#zjb<)5Yo%w4V#WK=a#-sIpaneQP-SC>{aS+D9i zHRB@V5>W~M1Fvf~Uziczz-lkwTj+3hOLIo&Wwj3bivf9Q94{1NPIYYExb#Q{XW`Ga zOComubgT2)?e1}A=jLZGKF*N4%boQ?dg;5wY<`^#LrEL`-)_p&eJ7jU`6s2?oBS%~ z`h>ih25-xRPu-EdD>3Wu=M_Eu3-Sy)FIDYhRrH?rR`J%gc01W~vC*1h1}mcOZhDcl zAo$eEn@75S*Tml!6In3lU-MRhgf(wAShfgd<~~~Saow(e3PSw;dI|+ER)4u&%yQe| z0>{RyM_CU1dUx=YK-11YbYg z+P~c|rNnJI+Y_Dm&`YnLrmZ&gEnbqf%C4*3j&h4UP@2^s-a((wW zo_zb}a$NQ7nSPhYuah?aYDj)kzoD(+&-!CQx3?d3%q;rqr)(YkEqTt@l-`qbDrN@1 z^O!Kf_Q9p;lfOI8eey8*SC<^4bmHTnL^D6GmfDrmZ#?F9tTcAsd2p%k?LfYYx2fBN z&llCpx6V4r!aG-O-zWEb2}RsC&sVxiHKadJePPSJ>Cd{&A%2|Ib<<`DOj|wc`n#Od zhd)jUTa;fPWb?!IiTryZt(ohWcB*Nr*K^vMn zY>p;KO}L?x-ZY8t_|X#!&ae1z_@1v7Tj<(wS^sl&9>?7NO|fqci{BF~^StMaKDTvr z&EFTjQ)3-+X8Ja5`zoMr7w}THG)t!C;kBJ^`gSGiOjBpH@6LXpVB_;*|HJlgk9{IH zZb&^-k>~X}c%}T!cV%*!7FSi?iKTl~3VmcN|0N}|`QfafD$Yn)zdhU6k$IN0lKm+Av)`{9R6g4* zXg>N#Xzr8KmYnftM4QX14{Y<^@J`L7z?wCEen{k|7mIq^MB16O@~%lNidSF99$h7+ z;d}ZB-_ze_Tqpgm$7)0@Y2^L#lY5H8Yt>KZ8E(jGcCRW7-&3~y)&kY^;0m zUssP!y;dJ|E^yWUsdE;;Jiho@EL+-P9)+BBGmokLQnluJDOPbyu}|D`!S%@7^&c|| zJ}}-pu=MKErs8tjI|<q64O1CAS zOQ+8*7GA)>!bGxoZq?B+iqLtN*&3i zQzs@y2X1RK@BKs9pjSRMmGh7i@=lXFwDd)nmMeWNR=ZG-f zG@Qc7U?DnN|Ep)B_C2#DMUBi$?Mj2p9NTL29Bo%?+`Z>~M&g)|#FL$t&JCNFufE&H z@m}Wg{`kqW8C>UGT(HB*U3F3J(vQlE531NPCHy(h^ZiHJZFvEqS5KI~WbgPmcV~%- zx>bVB_i}~<6>57Q>vC^1RXZ?q*$?w|8If5THy1d)ss7e-_2nLQ=T-R{Q=%8<@4smD zOp^Pnox@UziR;fCxiL{+aI!l4U8V)ge%&r~kL6~(-=b`G_3@>UZO5)`FA`(zXr8rA zNoMAmLnfU5#}+klGBh$i<*paA`XXsPJyq}pzwgq2NmGPath_HUzjRM6RQkAMr)64& z|K-T%R@)_?uwQQhe^2y zzY+ez6`6Bep-KOJvEISs#cH`^nkuoXVQboOFYs_vDFF@84#g4?ds#h||h@ z=a(j{Nh!L9Zrg8t|EK@2-;7Ou@4jeQuU=AJ?z)7} zd&P&vzRo2~^7<|^N};!_D&jcqYAz}gV5eFXt8p z7cm@?y^*r{P?K$GTIQ*hFZRTp+Ot+Z)hq14zN&q^;s;o7i(#$k8s|E%k}%&1}xoZ+Gp#dTjnG#e&>NR>!8Rvn_inq~qzuW|M1>Ja^87 z`)jUCYws(pd4J3#HmXOtz;=P_En$m)EGOEWzvZso8@AQ_n8>wRvkp!Q;W_%{>CPa( zFVYglm;QM#?`b(#Dw}9)!@Xff_JWh2Hyt&P6Vj|_(6XH-Xj=A<>Gf{O%^TY-E?4`C z#lF54HP`lL&qd+Gx%U&Ub$9RDTpSV4UbJs|$AMnHtc==iX4e&>I_IqNNZGl}$?B1t z!-w^*$0B!3HM#WKaG}+XpxU55JDl zXiB?!JgZ6W@6--%nKmo_=TjC~b8WJ>+30+;H2vZcFNdU9#~EfD_ve`OO4M0S*1G4o zb-|arL3doFQ?C5;cMRLEJWENgs&2|M*|`aKy>zq7j^1CtL@|N;%G9mwy3sp7Dw`>p z<-2V^Yq=-*`>(yCaq|qnsy~1Aq)Tt!3U8Lgy_}`x1}A^G>uj3;SnkXm&F3ty78+cu zeIxv)j(5A)+v3CLPF75H_I+ru{3YjogMjz!OpjjbwVvoOV=xhYeevzYGQG+FN*3PY z_HGus#w-0R@KBuhsT@sJMOXGawiOGf?lRiWqd!S<%FAkPQ|Y#+Piogs7S|Eu zr+$13?-OzUV@i3k+A?Rt_rKYzoW3k9(YZx(XI3)L@3-ZZoHG(-y#flJTs8ac`=@Vf z$U4sO-xredt(}wClFBs4pp z>oc>?PJ6%_w>hQkyUffEHs_z5IVsoIspjhGJ6R;<$D0_=^~maxYHZozB32&p@SWG1 zn-hPX7M#Qo!W|;l>(h8O%h9#lTXkJi#hRO+>U8Jtc=B}Hr`_B6n%w5-78>64kiM`= zME{h0LR8u`;lQN_Qtt6FRyluuvHkcmxA!*nCl1wAee8V`rk?u#NzSGXPTW3Sj|`^h z+-7{Bns=s7ZKK0AFTUOEleyy{Tr!4+ZHOlg1GKAnqRNW5-l}@=jW9vU`Kp zsg)T&1YT@P{mFg5@@e0vQw+=5ug28dx7}7>Hq&)+yyY%EH+84{*P;LPjZSQv`S@hp zu1|u8ny*U=rO3V0*_4#+9Db?JL*i@U*Os5BAN4OfZoBerm@w-@h4WiNYb@*UpSrUD z`+oi#9lD$H6ko6L{W)z#>2wQiZ8OJ9FNK5O96uCU;pf2Y(7 zZ1i94sK<$KfB&7=#5(yyY1)b;&tH$y4m{0WR=s4;q`%B2lB@6Xr&MJ~-MW2$W5O5T z4Z#(g(s!D>vAJ$_kyENtD9ZGB9yG=O&f%n4{zW#8nT^-FizAON*Ek}aC7AGiVStkT z`@fU8X5PQ1*XwIonEY|wz9;(gj|ND087=8Ez3`d;PKDUR4ZH`ko%@eFC4Xi~<5CC+ z5tOZzDpdNynVWvfe1fe^Q)hx&{*if-3F$fS*BrmJ$LyhaTkQq*hfIwDvd>E&n=u%l z39xqw{WLXqdBOtu^9~2Y<{WVBTOy&dXx6*;=cYfhFlAgS=PwpIrQ&RZ=W_d3r3Hcg z50Wf@usg)x++0vB^QL&II&15CnT}S4DR0is@vGi3xoxvda_3aN&iCme?=G{%&1329 zo+o#%RPS=@UYQT?<~9WvSKQ`|bJ726XBe}ta%14!=~w&%Z9T%z9oc)vu3I9cckALg zEIV8O-mDhM+`BC5WJHC;t$*Bkjd9F3D^I-R49<~UxIwdUDtbgy@w zul#ZQ#=IA*)n{VL5}W-Dj?ez`=Wb?z*$_{4I=M0e!I7Ac357#^5K?GTV=C1 zv%$YHGA`BMZ1Z;Zm_w|`+YG!Fk5x~ZFDdOgBk1zNmB#bYID+Hw7%xxaTp zHK+NVH@6tI)=hf$Z)bj=(mF>~?ccA2CoIoDew%mx@gpzp`1I{G`5nWy-@e3fLS}k9 zyNTI5!8qo^-ThyFoPCtQP$zmK!;9J0?-d{0SEt6%yXLn7-Y761{q*Af&D9spa;8r@ z_c?fq;EC6d7kloWWwlO6z`RU!&YJA4Zx-cQi*-(3eaAI&{!<-I_Tu-gK2P()xCMWR z#Z9)p!l{}$t8!wWl*;u9znVMx@D!16vQN6PuejJv@92XN?w?C9G4ILA^4xKHv2L|} zve&t`O`S=*4}W`~J1ec?#R3~iPt$q2l5EeFWxjtscJ6WJwwu?keO2CJ->~!5j`i*N z`)6kgw(|89ul&4b%DJ82jgDDHRsBhQdOn|1XyI{vg^(9(r$-s9IW%gn-$kr1WYjA8?W7(8@rj+%rirbch1+33w1u%R2fNbT6}Ha!+qM$ zo+aC@B?FEcShP6`?vK5H^v;8j=S=GlzH_~^WB%Ljn8}Y8oxR?zmOj;V>b_U?S6}5E zbD27=)6JUSWX2zTzfI!zY}cF%&;GsD()qc@O(FB3BOzPA$n4%%QuA^8)TN8{*DkA6 z+x=wnjr&DacM8&aKk4jP{HlN6UzgvnORi`g%ehe5|5KCgiup_KX8F6;p=!B2J1$9Z zGf$bCwzwvt^9ps!}XpHgq-pWbfJ*vT*sfqU9;=t1$Z