blob: a61e93097a174e38c0ca2739a3a2246b7cdee96e [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2020 Project CHIP Authors
#
# 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.
"""Flash an ESP32 device.
This is layered so that a caller can perform individual operations
through an `Flasher` instance, or operations according to a command line.
For `Flasher`, see the class documentation. For the parse_command()
interface or standalone execution:
usage: esp32_firmware_utils.py [-h] [--verbose] [--erase] [--application FILE]
[--verify_application] [--reset] [--skip_reset]
[--esptool FILE] [--parttool FILE]
[--sdkconfig FILE] [--chip CHIP] [--port PORT]
[--baud BAUD] [--before ACTION]
[--after ACTION] [--flash_mode MODE]
[--flash_freq FREQ] [--flash_size SIZE]
[--compress] [--bootloader FILE]
[--bootloader_offset OFFSET] [--partition FILE]
[--partition_offset OFFSET]
[--application_offset OFFSET]
Flash ESP32 device
optional arguments:
-h, --help show this help message and exit
configuration:
--verbose, -v Report more verbosely
--esptool FILE File name of the esptool executable
--parttool FILE File name of the parttool executable
--sdkconfig FILE File containing option defaults
--chip CHIP Target chip type
--port PORT Serial port device
--baud BAUD Serial port baud rate
--before ACTION What to do before connecting
--after ACTION What to do when finished
--flash_mode MODE, --flash-mode MODE
Flash mode
--flash_freq FREQ, --flash-freq FREQ
Flash frequency
--flash_size SIZE, --flash-size SIZE
Flash size
--compress, -z Compress data in transfer
--bootloader FILE Bootloader image
--bootloader_offset OFFSET, --bootloader-offset OFFSET
Bootloader offset
--partition FILE Partition table image
--partition_offset OFFSET, --partition-offset OFFSET
Partition table offset
--application_offset OFFSET, --application-offset OFFSET
Application offset
operations:
--erase Erase device
--application FILE Flash an image
--verify_application, --verify-application
Verify the image after flashing
--reset Reset device after flashing
--skip_reset, --skip-reset
Do not reset device after flashing
"""
import os
import pathlib
import sys
import firmware_utils
# Additional options that can be use to configure an `Flasher`
# object (as dictionary keys) and/or passed as command line options.
ESP32_OPTIONS = {
# Configuration options define properties used in flashing operations.
'configuration': {
# Tool configuration options.
'esptool': {
'help': 'File name of the esptool executable',
'default': None,
'argparse': {
'metavar': 'FILE',
},
'command': [
{'option': 'esptool'},
{'optional': 'port'},
{'optional': 'baud'},
{'optional': 'before'},
{'optional': 'after'},
()
],
'verify': ['{esptool}', 'version'],
'error':
"""\
Unable to execute {esptool}.
Please ensure that this tool is installed and
that $IDF_PATH is set. See the ESP32 example
README for installation instructions.
""",
},
'parttool': {
'help': 'File name of the parttool executable',
'default': None,
'argparse': {
'metavar': 'FILE'
},
'command': [
{'option': 'parttool'},
{'optional': 'port'},
{'optional': 'baud'},
{
'option': 'partition',
'result': ['--partition-table-file', '{partition}'],
'expand': True
},
()
],
'verify': ['{parttool}', '--quiet'],
'error':
"""\
Unable to execute {parttool}.
Please ensure that this tool is installed and
that $IDF_PATH is set. See the ESP32 example
README for installation instructions.
""",
},
'sdkconfig': {
'help': 'File containing option defaults',
'default': None,
'argparse': {
'metavar': 'FILE'
},
},
# Device configuration options.
'chip': {
'help': 'Target chip type',
'default': 'esp32',
'argparse': {
'metavar': 'CHIP'
},
'sdkconfig': 'CONFIG_IDF_TARGET',
},
'port': {
'help': 'Serial port device',
'default': None,
'argparse': {
'metavar': 'PORT',
},
'sdkconfig': 'CONFIG_ESPTOOLPY_PORT',
},
'baud': {
'help': 'Serial port baud rate',
'default': None,
'argparse': {
'metavar': 'BAUD',
},
'sdkconfig': 'CONFIG_ESPTOOLPY_BAUD',
},
'before': {
'help': 'What to do before connecting',
'default': None,
'argparse': {
'metavar': 'ACTION',
},
'sdkconfig': 'CONFIG_ESPTOOLPY_BEFORE',
},
'after': {
'help': 'What to do when finished',
'default': None,
'argparse': {
'metavar': 'ACTION',
},
'sdkconfig': 'CONFIG_ESPTOOLPY_AFTER',
},
'flash_mode': {
'help': 'Flash mode',
'default': None,
'argparse': {
'metavar': 'MODE',
},
'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHMODE',
},
'flash_freq': {
'help': 'Flash frequency',
'default': None,
'argparse': {
'metavar': 'FREQ',
},
'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHFREQ',
},
'flash_size': {
'help': 'Flash size',
'default': None,
'argparse': {
'metavar': 'SIZE',
},
'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHSIZE',
},
'compress': {
'help': 'Compress data in transfer',
'default': None,
'alias': ['-z'],
'argparse': {
'action': 'store_true'
},
'sdkconfig': 'CONFIG_ESPTOOLPY_COMPRESSED',
},
# Flashing things.
'bootloader': {
'help': 'Bootloader image',
'default': None,
'argparse': {
'metavar': 'FILE',
'type': pathlib.Path,
},
},
'bootloader_offset': {
'help': 'Bootloader offset',
'default': '0x1000',
'argparse': {
'metavar': 'OFFSET'
},
'sdkconfig': 'CONFIG_BOOTLOADER_OFFSET_IN_FLASH',
},
'partition': {
'help': 'Partition table image',
'default': None,
'argparse': {
'metavar': 'FILE',
'type': pathlib.Path,
},
},
'partition_offset': {
'help': 'Partition table offset',
'default': None,
'argparse': {
'metavar': 'OFFSET'
},
'sdkconfig': 'CONFIG_PARTITION_TABLE_OFFSET',
},
'application_offset': {
'help': 'Application offset',
'default': None,
'argparse': {
'metavar': 'OFFSET'
},
},
},
}
def namespace_defaults(dst, src):
for key, value in src.items():
if key not in dst or getattr(dst, key) is None:
setattr(dst, key, value)
class Flasher(firmware_utils.Flasher):
"""Manage esp32 flashing."""
def __init__(self, **options):
super().__init__(platform='ESP32', module=__name__, **options)
self.define_options(ESP32_OPTIONS)
def _postprocess_argv(self):
if self.option.sdkconfig:
namespace_defaults(self.option,
self.read_sdkconfig(self.option.sdkconfig))
# idf = os.environ.get('IDF_PATH')
# if idf:
# if self.option.esptool is None:
# self.option.esptool = os.path.join(
# idf,
# 'components/esptool_py/esptool/esptool.py')
# if self.option.parttool is None:
# self.option.parttool = os.path.join(
# idf,
# 'components/partition_table/parttool.py')
def read_sdkconfig(self, filename):
"""Given an ESP32 sdkconfig file, read it for values of options
not otherwise set.
"""
config_map = {}
for key, info in vars(self.info).items():
config_key = info.get('sdkconfig')
if config_key:
config_map[config_key] = key
result = {}
with open(filename) as f:
for line in f:
k, eq, v = line.strip().partition('=')
if eq == '=' and k in config_map:
result[config_map[k]] = v.strip('"')
return result
IDF_PATH_TOOLS = {
'esptool': 'components/esptool_py/esptool/esptool.py',
'parttool': 'components/partition_table/parttool.py',
}
def locate_tool(self, tool):
if tool in self.IDF_PATH_TOOLS:
idf_path = os.environ.get('IDF_PATH')
if idf_path:
return os.path.join(idf_path, self.IDF_PATH_TOOLS[tool])
return super().locate_tool(tool)
# Common command line arguments for esptool flashing subcommands.
FLASH_ARGUMENTS = [
{'optional': 'flash_mode'},
{'optional': 'flash_freq'},
{'optional': 'flash_size'},
{
'match': '{compress}',
'test': [(True, '-z'), ('y', '-z')],
'default': '-u'
},
]
def erase(self):
"""Perform `commander device masserase`."""
return self.run_tool('esptool', ['erase_flash'], {}, 'Erase device')
def verify(self, image, address=0):
"""Verify image(s)."""
if not isinstance(image, list):
image = [address, image]
return self.run_tool(
'esptool',
['verify_flash', self.FLASH_ARGUMENTS, image],
name='Verify',
pass_message='Verified',
fail_message='Not verified',
fail_level=2)
def flash(self, image, address=0):
"""Flash image(s)."""
if not isinstance(image, list):
image = [address, image]
return self.run_tool(
'esptool',
['write_flash', self.FLASH_ARGUMENTS, image],
name='Flash')
PARTITION_INFO = {
'phy': ['--partition-type', 'data', '--partition-subtype', 'phy'],
'application': ['--partition-boot-default'],
'ota': ['--partition-type', 'data', '--partition-subtype', 'ota'],
'factory': [
'--partition-type', 'app', '--partition-subtype', 'factory'
],
}
def get_partition_info(self, item, info, options=None):
"""Run parttool to get partition information."""
return self.run_tool(
'parttool',
['get_partition_info', self.PARTITION_INFO[item], '--info', info],
options or {},
capture_output=True).stdout.strip()
def make_wrapper(self, argv):
self.parser.add_argument(
'--use-parttool',
metavar='FILENAME',
help='partition tool to configure flashing script')
self.parser.add_argument(
'--use-partition-file',
metavar='FILENAME',
help='partition file to configure flashing script')
self.parser.add_argument(
'--use-sdkconfig',
metavar='FILENAME',
help='sdkconfig to configure flashing script')
super().make_wrapper(argv)
def _platform_wrapper_args(self, args):
if args.use_sdkconfig:
# Include values from sdkconfig so that it isn't needed at
# flashing time.
namespace_defaults(args, self.read_sdkconfig(args.use_sdkconfig))
parttool = args.use_parttool or args.parttool
partfile = args.use_partition_file or args.partition
if parttool and partfile:
# Get unspecified offsets from the partition file now,
# so that parttool isn't needed at flashing time.
if args.application and args.application_offset is None:
args.application_offset = self.get_partition_info(
'application', 'offset',
{'parttool': parttool, 'partition': partfile})
def actions(self):
"""Perform actions on the device according to self.option."""
self.log(3, 'Options:', self.option)
if self.option.erase:
if self.erase().err:
return self
if self.option.application:
application = self.option.application
bootloader = self.option.bootloader
partition = self.option.partition
# Collect the flashable items.
flash = []
if bootloader:
flash += [self.option.bootloader_offset, bootloader]
if application:
offset = self.option.application_offset
if offset is None:
offset = self.get_partition_info('application', 'offset')
flash += [offset, application]
if partition:
flash += [self.option.partition_offset, partition]
# esptool.py doesn't have an independent reset command, so we add
# an `--after` option to the final operation, which may be either
# flash or verify.
if self.option.after is None:
if self.option.reset or self.option.reset is None:
self.option.after = 'hard_reset'
else:
self.option.after = 'no_reset'
if self.option.verify_application:
verify_after = self.option.after
self.option.after = 'no_reset'
if self.flash(flash).err:
return self
if self.option.verify_application:
self.option.after = verify_after
if self.verify(flash).err:
return self
return self
# Mobly integration
class ESP32Platform:
def __init__(self, flasher_args):
self.flasher = Flasher(**flasher_args)
def flash(self):
self.flasher.flash_command([os.getcwd()])
def verify_platform_args(platform_args):
required_args = [
'application',
'parttool',
'port',
'baud',
'before',
'after',
'flash_mode',
'flash_freq',
'flash_size',
'compress',
'bootloader',
'partition',
'partition_offset',
'application_offset',
]
difference = set(required_args) - set(platform_args)
if difference:
raise ValueError("Required arguments missing: %s" % difference)
def create_platform(platform_args):
verify_platform_args(platform_args[0])
return ESP32Platform(platform_args[0])
# End of Mobly integration
if __name__ == '__main__':
sys.exit(Flasher().flash_command(sys.argv))