mirror of
https://github.com/espressif/esp-idf.git
synced 2026-07-17 00:13:06 +03:00
544 lines
18 KiB
Python
Executable File
544 lines
18 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# parttool is used to perform partition level operations - reading,
|
|
# writing, erasing and getting info about the partition.
|
|
#
|
|
# SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
import gen_esp32part as gen
|
|
import rich_click as click
|
|
from esp_pylib.cli_options import EspRichGroup
|
|
from esp_pylib.cli_options import MutuallyExclusiveOption
|
|
from esp_pylib.cli_options import OptionEatAll
|
|
from esp_pylib.cli_types import AnyIntType
|
|
from esp_pylib.logger import log
|
|
|
|
__version__ = '2.2'
|
|
|
|
COMPONENTS_PATH = os.path.expandvars(os.path.join('$IDF_PATH', 'components'))
|
|
|
|
PARTITION_TABLE_OFFSET = 0x8000
|
|
|
|
|
|
class _StringListParamType(click.ParamType):
|
|
"""ParamType for `OptionEatAll` without ``multiple=True`` (argparse ``nargs='+'``)."""
|
|
|
|
name = 'text_list'
|
|
|
|
def convert(self, value, param, ctx):
|
|
if not value:
|
|
return ()
|
|
if isinstance(value, (list, tuple)): # noqa: UP038
|
|
return tuple(value)
|
|
return (value,)
|
|
|
|
|
|
_ESPTOOL_ARGS_TYPE = _StringListParamType()
|
|
|
|
|
|
quiet = False
|
|
|
|
|
|
def status(msg):
|
|
if not quiet:
|
|
log.print(msg, file=sys.stderr)
|
|
|
|
|
|
class _PartitionId:
|
|
def __init__(self, name=None, p_type=None, subtype=None, part_list=None):
|
|
self.name = name
|
|
self.type = p_type
|
|
self.subtype = subtype
|
|
self.part_list = part_list
|
|
|
|
|
|
class PartitionName(_PartitionId):
|
|
def __init__(self, name):
|
|
_PartitionId.__init__(self, name=name)
|
|
|
|
|
|
class PartitionType(_PartitionId):
|
|
def __init__(self, p_type, subtype, part_list=None):
|
|
_PartitionId.__init__(self, p_type=p_type, subtype=subtype, part_list=part_list)
|
|
|
|
|
|
PARTITION_BOOT_DEFAULT = _PartitionId()
|
|
|
|
|
|
class ParttoolTarget:
|
|
def __init__(
|
|
self,
|
|
port=None,
|
|
baud=None,
|
|
partition_table_offset=PARTITION_TABLE_OFFSET,
|
|
primary_bootloader_offset=None,
|
|
recovery_bootloader_offset=None,
|
|
partition_table_file=None,
|
|
esptool_args=[],
|
|
esptool_write_args=[],
|
|
esptool_read_args=[],
|
|
esptool_erase_args=[],
|
|
):
|
|
self.port = port
|
|
self.baud = baud
|
|
|
|
gen.offset_part_table = partition_table_offset
|
|
gen.primary_bootloader_offset = primary_bootloader_offset
|
|
gen.recovery_bootloader_offset = recovery_bootloader_offset
|
|
gen.quiet = True
|
|
|
|
def parse_esptool_args(esptool_args):
|
|
results = list()
|
|
for arg in esptool_args:
|
|
pattern = re.compile(r'(.+)=(.+)')
|
|
result = pattern.match(arg)
|
|
try:
|
|
key = result.group(1)
|
|
value = result.group(2)
|
|
results.extend(['--' + key, value])
|
|
except AttributeError:
|
|
results.extend(['--' + arg])
|
|
return results
|
|
|
|
self.esptool_args = parse_esptool_args(esptool_args)
|
|
self.esptool_write_args = parse_esptool_args(esptool_write_args)
|
|
self.esptool_read_args = parse_esptool_args(esptool_read_args)
|
|
self.esptool_erase_args = parse_esptool_args(esptool_erase_args)
|
|
|
|
if partition_table_file:
|
|
with open(partition_table_file, 'rb') as f:
|
|
partition_table, _ = gen.PartitionTable.from_file(f)
|
|
else:
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
temp_file.close()
|
|
|
|
try:
|
|
self._call_esptool(
|
|
['read-flash', str(partition_table_offset), str(gen.MAX_PARTITION_LENGTH), temp_file.name]
|
|
)
|
|
with open(temp_file.name, 'rb') as f:
|
|
partition_table = gen.PartitionTable.from_binary(f.read())
|
|
finally:
|
|
os.unlink(temp_file.name)
|
|
|
|
self.partition_table = partition_table
|
|
|
|
# set `out` to None to redirect the output to the STDOUT
|
|
# otherwise set `out` to file descriptor
|
|
# beware that the method does not close the file descriptor
|
|
def _call_esptool(self, args, out=None):
|
|
esptool_args = [sys.executable, '-m', 'esptool'] + self.esptool_args
|
|
|
|
if self.port:
|
|
esptool_args += ['--port', self.port]
|
|
|
|
if self.baud:
|
|
esptool_args += ['--baud', str(self.baud)]
|
|
|
|
esptool_args += args
|
|
|
|
log.print(f'Running {" ".join(esptool_args)}...', file=sys.stderr)
|
|
try:
|
|
subprocess.check_call(esptool_args, stdout=out, stderr=subprocess.STDOUT)
|
|
except subprocess.CalledProcessError as e:
|
|
log.print(f'An exception: ** {e} ** occurred in _call_esptool.', file=out or sys.stderr)
|
|
raise e
|
|
|
|
def get_partition_info(self, partition_id):
|
|
partition = None
|
|
|
|
if partition_id.name:
|
|
partition = self.partition_table.find_by_name(partition_id.name)
|
|
elif partition_id.type and partition_id.subtype:
|
|
partition = list(self.partition_table.find_by_type(partition_id.type, partition_id.subtype))
|
|
if not partition_id.part_list:
|
|
partition = partition[0]
|
|
else: # default boot partition
|
|
search = ['factory'] + [f'ota_{d}' for d in range(16)]
|
|
for subtype in search:
|
|
partition = next(self.partition_table.find_by_type('app', subtype), None)
|
|
if partition:
|
|
break
|
|
|
|
if not partition:
|
|
raise Exception('Partition does not exist')
|
|
|
|
return partition
|
|
|
|
def erase_partition(self, partition_id):
|
|
partition = self.get_partition_info(partition_id)
|
|
self._call_esptool(['erase-region', str(partition.offset), str(partition.size)] + self.esptool_erase_args)
|
|
|
|
def read_partition(self, partition_id, output):
|
|
partition = self.get_partition_info(partition_id)
|
|
self._call_esptool(['read-flash', str(partition.offset), str(partition.size), output] + self.esptool_read_args)
|
|
|
|
def write_partition(self, partition_id, input, ignore_readonly=False): # noqa: A002
|
|
partition = self.get_partition_info(partition_id)
|
|
|
|
if partition.readonly and not ignore_readonly:
|
|
log.die(f'"{partition.name}" partition is read-only, (use the --ignore-readonly flag to skip it)')
|
|
|
|
self.erase_partition(partition_id)
|
|
|
|
with open(input, 'rb') as f:
|
|
content_len = len(f.read())
|
|
|
|
if content_len > partition.size:
|
|
raise Exception('Input file size exceeds partition size')
|
|
|
|
self._call_esptool(['write-flash', str(partition.offset), input] + self.esptool_write_args)
|
|
|
|
|
|
def _write_partition(target, partition_id, input, ignore_readonly=False): # noqa: A002
|
|
target.write_partition(partition_id, input, ignore_readonly)
|
|
partition = target.get_partition_info(partition_id)
|
|
status(f"Written contents of file '{input}' at offset 0x{partition.offset:x}")
|
|
|
|
|
|
def _read_partition(target, partition_id, output):
|
|
target.read_partition(partition_id, output)
|
|
partition = target.get_partition_info(partition_id)
|
|
status(
|
|
f"Read partition '{partition.name}' contents from device at offset 0x{partition.offset:x} to file '{output}'"
|
|
)
|
|
|
|
|
|
def _erase_partition(target, partition_id):
|
|
target.erase_partition(partition_id)
|
|
partition = target.get_partition_info(partition_id)
|
|
status(f"Erased partition '{partition.name}' at offset 0x{partition.offset:x}")
|
|
|
|
|
|
def _get_partition_info(target, partition_id, info):
|
|
try:
|
|
partitions = target.get_partition_info(partition_id)
|
|
if not isinstance(partitions, list):
|
|
partitions = [partitions]
|
|
except Exception:
|
|
return
|
|
|
|
infos = []
|
|
|
|
try:
|
|
for p in partitions:
|
|
info_dict = {
|
|
'name': f'{p.name}',
|
|
'type': f'{p.type}',
|
|
'subtype': f'{p.subtype}',
|
|
'offset': f'0x{p.offset:x}',
|
|
'size': f'0x{p.size:x}',
|
|
'encrypted': f'{p.encrypted}',
|
|
'readonly': f'{p.readonly}',
|
|
}
|
|
for i in info:
|
|
infos += [info_dict[i]]
|
|
except KeyError:
|
|
log.die(f'Request for unknown partition info {i}')
|
|
|
|
print(' '.join(infos))
|
|
|
|
|
|
def _partition_selection_options(func):
|
|
decorators = [
|
|
click.option(
|
|
'--partition-name',
|
|
'-n',
|
|
cls=MutuallyExclusiveOption,
|
|
exclusive_with=['partition_type', 'partition_boot_default'],
|
|
help='name of the partition',
|
|
),
|
|
click.option(
|
|
'--partition-type',
|
|
'-t',
|
|
cls=MutuallyExclusiveOption,
|
|
exclusive_with=['partition_name', 'partition_boot_default'],
|
|
help='type of the partition',
|
|
),
|
|
click.option(
|
|
'--partition-boot-default',
|
|
'-d',
|
|
is_flag=True,
|
|
cls=MutuallyExclusiveOption,
|
|
exclusive_with=['partition_name', 'partition_type'],
|
|
help='select the default boot partition using the same fallback logic as the IDF bootloader',
|
|
),
|
|
click.option('--partition-subtype', '-s', help='subtype of the partition'),
|
|
# Use OptionEatAll to keep argparse nargs='*' semantics: a single
|
|
# --extra-partition-subtypes flag consumes all following space-separated
|
|
# entries (CMake project_include.cmake passes them this way).
|
|
click.option(
|
|
'--extra-partition-subtypes',
|
|
multiple=True,
|
|
cls=OptionEatAll,
|
|
type=str,
|
|
help='Extra partition subtype entries',
|
|
),
|
|
]
|
|
for decorator in reversed(decorators):
|
|
func = decorator(func)
|
|
return func
|
|
|
|
|
|
def _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default, part_list=False):
|
|
if partition_name:
|
|
return PartitionName(partition_name)
|
|
if partition_type:
|
|
if not partition_subtype:
|
|
log.die('--partition-subtype should be defined when --partition-type is defined')
|
|
return PartitionType(partition_type, partition_subtype, part_list)
|
|
if partition_boot_default:
|
|
return PARTITION_BOOT_DEFAULT
|
|
log.die(
|
|
'Partition to operate on should be defined using --partition-name OR '
|
|
'partition-type,--partition-subtype OR partition-boot-default'
|
|
)
|
|
|
|
|
|
def _target_kwargs_from_ctx(ctx_obj, extra_partition_subtypes=None):
|
|
kwargs = {}
|
|
for key, value in (
|
|
('port', ctx_obj.get('port')),
|
|
('baud', ctx_obj.get('baud')),
|
|
('partition_table_offset', ctx_obj.get('partition_table_offset')),
|
|
('primary_bootloader_offset', ctx_obj.get('primary_bootloader_offset')),
|
|
('recovery_bootloader_offset', ctx_obj.get('recovery_bootloader_offset')),
|
|
('partition_table_file', ctx_obj.get('partition_table_file')),
|
|
('esptool_args', ctx_obj.get('esptool_args')),
|
|
('esptool_write_args', ctx_obj.get('esptool_write_args')),
|
|
('esptool_read_args', ctx_obj.get('esptool_read_args')),
|
|
('esptool_erase_args', ctx_obj.get('esptool_erase_args')),
|
|
):
|
|
if value is not None and value != ():
|
|
kwargs[key] = value
|
|
if extra_partition_subtypes:
|
|
gen.add_extra_subtypes(extra_partition_subtypes)
|
|
return kwargs
|
|
|
|
|
|
def _run_operation(operation, quiet_flag, target, partition_id, **op_kwargs):
|
|
global quiet
|
|
|
|
quiet = quiet_flag
|
|
|
|
parttool_ops = {
|
|
'erase_partition': (_erase_partition, []),
|
|
'read_partition': (_read_partition, ['output']),
|
|
'write_partition': (_write_partition, ['input', 'ignore_readonly']),
|
|
'get_partition_info': (_get_partition_info, ['info']),
|
|
}
|
|
|
|
op, op_arg_names = parttool_ops[operation]
|
|
common_args = {'target': target, 'partition_id': partition_id}
|
|
for op_arg in op_arg_names:
|
|
common_args[op_arg] = op_kwargs[op_arg]
|
|
|
|
if quiet:
|
|
try:
|
|
op(**common_args)
|
|
except Exception:
|
|
sys.exit(2)
|
|
else:
|
|
try:
|
|
op(**common_args)
|
|
except gen.InputError as e:
|
|
log.print(str(e), file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
|
|
@click.group(
|
|
cls=EspRichGroup,
|
|
context_settings={'help_option_names': ['-h', '--help']},
|
|
help='ESP-IDF Partitions Tool',
|
|
)
|
|
@click.option('--quiet', '-q', is_flag=True, help='suppress stderr messages')
|
|
@click.option(
|
|
'--esptool-args',
|
|
cls=OptionEatAll,
|
|
type=_ESPTOOL_ARGS_TYPE,
|
|
default=(),
|
|
help='additional main arguments for esptool',
|
|
)
|
|
@click.option(
|
|
'--esptool-write-args',
|
|
cls=OptionEatAll,
|
|
type=_ESPTOOL_ARGS_TYPE,
|
|
default=(),
|
|
help='additional subcommand arguments when writing to flash',
|
|
)
|
|
@click.option(
|
|
'--esptool-read-args',
|
|
cls=OptionEatAll,
|
|
type=_ESPTOOL_ARGS_TYPE,
|
|
default=(),
|
|
help='additional subcommand arguments when reading flash',
|
|
)
|
|
@click.option(
|
|
'--esptool-erase-args',
|
|
cls=OptionEatAll,
|
|
type=_ESPTOOL_ARGS_TYPE,
|
|
default=(),
|
|
help='additional subcommand arguments when erasing regions of flash',
|
|
)
|
|
@click.option(
|
|
'--port',
|
|
'-p',
|
|
help='port where the target device of the command is connected to; the partition table is sourced from '
|
|
'this device when the partition table file is not defined',
|
|
)
|
|
@click.option('--baud', '-b', type=int, help='baudrate to use')
|
|
@click.option('--partition-table-offset', '-o', type=AnyIntType(), help='offset to read the partition table from')
|
|
@click.option('--primary-bootloader-offset', type=AnyIntType(), help='offset for primary bootloader')
|
|
@click.option('--recovery-bootloader-offset', type=AnyIntType(), help='offset for recovery bootloader')
|
|
@click.option(
|
|
'--partition-table-file',
|
|
'-f',
|
|
type=click.Path(),
|
|
help='file (CSV/binary) to read the partition table from; '
|
|
'overrides device attached to specified port as the partition table source when defined',
|
|
)
|
|
@click.pass_context
|
|
def cli(
|
|
ctx,
|
|
quiet,
|
|
esptool_args,
|
|
esptool_write_args,
|
|
esptool_read_args,
|
|
esptool_erase_args,
|
|
port,
|
|
baud,
|
|
partition_table_offset,
|
|
primary_bootloader_offset,
|
|
recovery_bootloader_offset,
|
|
partition_table_file,
|
|
):
|
|
ctx.ensure_object(dict)
|
|
ctx.obj.update(
|
|
{
|
|
'quiet': quiet,
|
|
'esptool_args': esptool_args,
|
|
'esptool_write_args': esptool_write_args,
|
|
'esptool_read_args': esptool_read_args,
|
|
'esptool_erase_args': esptool_erase_args,
|
|
'port': port,
|
|
'baud': baud,
|
|
'partition_table_offset': partition_table_offset,
|
|
'primary_bootloader_offset': primary_bootloader_offset,
|
|
'recovery_bootloader_offset': recovery_bootloader_offset,
|
|
'partition_table_file': partition_table_file,
|
|
}
|
|
)
|
|
|
|
|
|
def _shared_subcommand_options(func):
|
|
func = click.pass_context(func)
|
|
return func
|
|
|
|
|
|
@cli.command('read_partition', help='read partition from device and dump contents into a file')
|
|
@_partition_selection_options
|
|
@click.option('--output', help='file to dump the read partition contents to')
|
|
@_shared_subcommand_options
|
|
def read_partition_cmd(
|
|
ctx,
|
|
partition_name,
|
|
partition_type,
|
|
partition_boot_default,
|
|
partition_subtype,
|
|
extra_partition_subtypes,
|
|
output,
|
|
):
|
|
partition_id = _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default)
|
|
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
|
|
_run_operation('read_partition', ctx.obj['quiet'], target, partition_id, output=output)
|
|
|
|
|
|
@cli.command('write_partition', help='write contents of a binary file to partition on device')
|
|
@_partition_selection_options
|
|
@click.option('--input', 'input_file', help='file whose contents are to be written to the partition offset')
|
|
@click.option('--ignore-readonly', is_flag=True, help='Ignore read-only attribute')
|
|
@_shared_subcommand_options
|
|
def write_partition_cmd(
|
|
ctx,
|
|
partition_name,
|
|
partition_type,
|
|
partition_boot_default,
|
|
partition_subtype,
|
|
extra_partition_subtypes,
|
|
input_file,
|
|
ignore_readonly,
|
|
):
|
|
partition_id = _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default)
|
|
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
|
|
_run_operation(
|
|
'write_partition',
|
|
ctx.obj['quiet'],
|
|
target,
|
|
partition_id,
|
|
input=input_file,
|
|
ignore_readonly=ignore_readonly,
|
|
)
|
|
|
|
|
|
@cli.command('erase_partition', help='erase the contents of a partition on the device')
|
|
@_partition_selection_options
|
|
@_shared_subcommand_options
|
|
def erase_partition_cmd(
|
|
ctx,
|
|
partition_name,
|
|
partition_type,
|
|
partition_boot_default,
|
|
partition_subtype,
|
|
extra_partition_subtypes,
|
|
):
|
|
partition_id = _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default)
|
|
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
|
|
_run_operation('erase_partition', ctx.obj['quiet'], target, partition_id)
|
|
|
|
|
|
@cli.command('get_partition_info', help='get partition information')
|
|
@_partition_selection_options
|
|
@click.option(
|
|
'--info',
|
|
type=click.Choice(['name', 'type', 'subtype', 'offset', 'size', 'encrypted', 'readonly']),
|
|
multiple=True,
|
|
cls=OptionEatAll,
|
|
default=['offset', 'size'],
|
|
show_default=True,
|
|
help='type of partition information to get',
|
|
)
|
|
@click.option('--part_list', 'part_list', is_flag=True, help='Get a list of partitions suitable for a given type')
|
|
@_shared_subcommand_options
|
|
def get_partition_info_cmd(
|
|
ctx,
|
|
partition_name,
|
|
partition_type,
|
|
partition_boot_default,
|
|
partition_subtype,
|
|
extra_partition_subtypes,
|
|
info,
|
|
part_list,
|
|
):
|
|
partition_id = _build_partition_id(
|
|
partition_name, partition_type, partition_subtype, partition_boot_default, part_list=part_list
|
|
)
|
|
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
|
|
_run_operation('get_partition_info', ctx.obj['quiet'], target, partition_id, info=info)
|
|
|
|
|
|
def main():
|
|
cli()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
from esp_pylib.excepthook import install_exception_reporting
|
|
|
|
install_exception_reporting()
|
|
main()
|