# SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import json import os import shlex import signal import sys from typing import Any from typing import Dict from typing import List from typing import Optional import click from idf_py_actions.global_options import global_options from idf_py_actions.tools import ensure_build_directory from idf_py_actions.tools import get_default_serial_port from idf_py_actions.tools import get_sdkconfig_value from idf_py_actions.tools import PropertyDict from idf_py_actions.tools import run_target from idf_py_actions.tools import RunTool PYTHON = sys.executable BAUD_RATE = { 'names': ['-b', '--baud'], 'help': 'Baud rate for flashing. It can imply monitor baud rate as well if it hasn\'t been defined locally.', 'scope': 'global', 'envvar': 'ESPBAUD', 'default': 460800, } PORT = { 'names': ['-p', '--port'], 'help': 'Serial port.', 'scope': 'global', 'envvar': 'ESPPORT', 'type': click.Path(), 'default': None, } def yellow_print(message: str, newline: Optional[str]='\n') -> None: """Print a message to stderr with yellow highlighting """ sys.stderr.write('%s%s%s%s' % ('\033[0;33m', message, '\033[0m', newline)) sys.stderr.flush() def action_extensions(base_actions: Dict, project_path: str) -> Dict: def _get_project_desc(ctx: click.core.Context, args: PropertyDict) -> Any: desc_path = os.path.join(args.build_dir, 'project_description.json') if not os.path.exists(desc_path): ensure_build_directory(args, ctx.info_name) with open(desc_path, 'r') as f: project_desc = json.load(f) return project_desc def _get_esptool_args(args: PropertyDict) -> List: esptool_path = os.path.join(os.environ['IDF_PATH'], 'components/esptool_py/esptool/esptool.py') esptool_wrapper_path = os.environ.get('ESPTOOL_WRAPPER', '') if args.port is None: args.port = get_default_serial_port() result = [PYTHON] if os.path.exists(esptool_wrapper_path): result += [esptool_wrapper_path] result += [esptool_path] result += ['-p', args.port] result += ['-b', str(args.baud)] with open(os.path.join(args.build_dir, 'flasher_args.json')) as f: flasher_args = json.load(f) extra_esptool_args = flasher_args['extra_esptool_args'] result += ['--before', extra_esptool_args['before']] result += ['--after', extra_esptool_args['after']] result += ['--chip', extra_esptool_args['chip']] if not extra_esptool_args['stub']: result += ['--no-stub'] return result def _get_commandline_options(ctx: click.core.Context) -> List: """ Return all the command line options up to first action """ # This approach ignores argument parsing done Click result = [] for arg in sys.argv: if arg in ctx.command.commands_with_aliases: break result.append(arg) return result def monitor(action: str, ctx: click.core.Context, args: PropertyDict, print_filter: str, monitor_baud: str, encrypted: bool, no_reset: bool, timestamps: bool, timestamp_format: str, force_color: bool) -> None: """ Run esp_idf_monitor to watch build output """ project_desc = _get_project_desc(ctx, args) elf_file = os.path.join(args.build_dir, project_desc['app_elf']) idf_monitor = os.path.join(os.environ['IDF_PATH'], 'tools/idf_monitor.py') monitor_args = [PYTHON, idf_monitor] if project_desc['target'] != 'linux': if no_reset and args.port is None: msg = ('WARNING: --no-reset is ignored. ' 'Please specify the port with the --port argument in order to use this option.') yellow_print(msg) no_reset = False args.port = args.port or get_default_serial_port() monitor_args += ['-p', args.port] baud = monitor_baud or os.getenv('IDF_MONITOR_BAUD') or os.getenv('MONITORBAUD') if baud is None: # Baud hasn't been changed locally (by local baud argument nor by environment variables) # # Use the global baud rate if it has been changed by the command line. # Use project_desc['monitor_baud'] as the last option. global_baud_defined = ctx._parameter_source['baud'] == click.core.ParameterSource.COMMANDLINE baud = args.baud if global_baud_defined else project_desc['monitor_baud'] monitor_args += ['-b', baud] monitor_args += ['--toolchain-prefix', project_desc['monitor_toolprefix']] coredump_decode = get_sdkconfig_value(project_desc['config_file'], 'CONFIG_ESP_COREDUMP_DECODE') if coredump_decode is not None: monitor_args += ['--decode-coredumps', coredump_decode] target_arch_riscv = get_sdkconfig_value(project_desc['config_file'], 'CONFIG_IDF_TARGET_ARCH_RISCV') monitor_args += ['--target', project_desc['target']] revision = project_desc.get('min_rev') if revision: monitor_args += ['--revision', revision] if target_arch_riscv: monitor_args += ['--decode-panic', 'backtrace'] if print_filter is not None: monitor_args += ['--print_filter', print_filter] if elf_file: monitor_args += [elf_file] if encrypted: monitor_args += ['--encrypted'] if no_reset: monitor_args += ['--no-reset'] if timestamps: monitor_args += ['--timestamps'] if timestamp_format: monitor_args += ['--timestamp-format', timestamp_format] if force_color or os.name == 'nt': monitor_args += ['--force-color'] idf_py = [PYTHON] + _get_commandline_options(ctx) # commands to re-run idf.py monitor_args += ['-m', ' '.join("'%s'" % a for a in idf_py)] hints = not args.no_hints # Temporally ignore SIGINT, which is used in idf_monitor to spawn gdb. old_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, signal.SIG_IGN) try: RunTool('idf_monitor', monitor_args, args.project_dir, build_dir=args.build_dir, hints=hints, interactive=True, convert_output=True)() finally: signal.signal(signal.SIGINT, old_handler) def flash(action: str, ctx: click.core.Context, args: PropertyDict, force: bool, extra_args: str) -> None: """ Run esptool to flash the entire project, from an argfile generated by the build system """ ensure_build_directory(args, ctx.info_name) project_desc = _get_project_desc(ctx, args) if project_desc['target'] == 'linux': yellow_print('skipping flash since running on linux...') return args.port = args.port or get_default_serial_port() extra = list() if force: extra.append('--force') if extra_args: extra += shlex.split(extra_args) env = {'ESPBAUD': str(args.baud), 'ESPPORT': args.port, 'SERIAL_TOOL_EXTRA_ARGS': ';'.join(extra)} run_target(action, args, env, force_progression=True) def erase_flash(action: str, ctx: click.core.Context, args: PropertyDict) -> None: ensure_build_directory(args, ctx.info_name) esptool_args = _get_esptool_args(args) esptool_args += ['erase_flash'] RunTool('esptool.py', esptool_args, args.build_dir, hints=not args.no_hints)() def global_callback(ctx: click.core.Context, global_args: Dict, tasks: PropertyDict) -> None: encryption = any([task.name in ('encrypted-flash', 'encrypted-app-flash') for task in tasks]) if encryption: for task in tasks: if task.name == 'monitor': task.action_args['encrypted'] = True break def ota_targets(target_name: str, ctx: click.core.Context, args: PropertyDict) -> None: """ Execute the target build system to build target 'target_name'. Additionally set global variables for baud and port. Calls ensure_build_directory() which will run cmake to generate a build directory (with the specified generator) as needed. """ args.port = args.port or get_default_serial_port() ensure_build_directory(args, ctx.info_name) run_target(target_name, args, {'ESPBAUD': str(args.baud), 'ESPPORT': args.port}) def merge_bin(action: str, ctx: click.core.Context, args: PropertyDict, output: str, format: str, md5_disable: str, flash_offset: str, fill_flash_size: str) -> None: ensure_build_directory(args, ctx.info_name) project_desc = _get_project_desc(ctx, args) merge_bin_args = [PYTHON, '-m', 'esptool'] target = project_desc['target'] merge_bin_args += ['--chip', target] merge_bin_args += ['merge_bin'] # needs to be after the --chip option if not output: if format in ('raw', 'uf2'): output = 'merged-binary.bin' elif format == 'hex': output = 'merged-binary.hex' merge_bin_args += ['-o', output] if format: merge_bin_args += ['-f', format] if md5_disable: if format != 'uf2': yellow_print('idf.py merge-bin: --md5-disable is only valid for UF2 format. Option will be ignored.') else: merge_bin_args += ['--md5-disable'] if flash_offset: if format != 'raw': yellow_print('idf.py merge-bin: --flash-offset is only valid for RAW format. Option will be ignored.') else: merge_bin_args += ['-t', flash_offset] if fill_flash_size: if format != 'raw': yellow_print('idf.py merge-bin: --fill-flash-size is only valid for RAW format, option will be ignored.') else: merge_bin_args += ['--fill-flash-size', fill_flash_size] merge_bin_args += ['@flash_args'] print(f'Merged binary {output} will be created in the build directory...') RunTool('merge_bin', merge_bin_args, args.build_dir, build_dir=args.build_dir, hints=not args.no_hints)() def secure_decrypt_flash_data( action: str, ctx: click.core.Context, args: PropertyDict, aes_xts: bool, keyfile: str, output: str, address: str, flash_crypt_conf: str, **extra_args: str) -> None: ensure_build_directory(args, ctx.info_name) decrypt_flash_data_args = [PYTHON, '-m', 'espsecure', 'decrypt_flash_data'] if aes_xts: decrypt_flash_data_args += ['--aes_xts'] if keyfile: decrypt_flash_data_args += ['--keyfile', keyfile] if output: decrypt_flash_data_args += ['--output', output] if address: decrypt_flash_data_args += ['--address', address] if flash_crypt_conf: decrypt_flash_data_args += ['--flash_crypt_conf', flash_crypt_conf] if extra_args['encrypted_file']: decrypt_flash_data_args += [extra_args['encrypted_file']] RunTool('espsecure', decrypt_flash_data_args, args.build_dir)() def secure_digest_secure_bootloader( action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, output: str, iv: str, **extra_args: str) -> None: ensure_build_directory(args, ctx.info_name) digest_secure_bootloader_args = [PYTHON, '-m', 'espsecure', 'digest_secure_bootloader'] if keyfile: digest_secure_bootloader_args += ['--keyfile', keyfile] if output: digest_secure_bootloader_args += ['--output', output] if iv: digest_secure_bootloader_args += ['--iv', iv] if extra_args['image']: digest_secure_bootloader_args += [extra_args['image']] RunTool('espsecure', digest_secure_bootloader_args, args.build_dir)() def secure_encrypt_flash_data( action: str, ctx: click.core.Context, args: PropertyDict, aes_xts: bool, keyfile: str, output: str, address: str, flash_crypt_conf: str, **extra_args: str) -> None: ensure_build_directory(args, ctx.info_name) encrypt_flash_data_args = [PYTHON, '-m', 'espsecure', 'encrypt_flash_data'] if aes_xts: encrypt_flash_data_args += ['--aes_xts'] if keyfile: encrypt_flash_data_args += ['--keyfile', keyfile] if output: encrypt_flash_data_args += ['--output', output] if address: encrypt_flash_data_args += ['--address', address] if flash_crypt_conf: encrypt_flash_data_args += ['--flash_crypt_conf', flash_crypt_conf] if extra_args['plaintext_file']: encrypt_flash_data_args += [extra_args['plaintext_file']] RunTool('espsecure', encrypt_flash_data_args, args.build_dir)() def secure_generate_flash_encryption_key(action: str, ctx: click.core.Context, args: PropertyDict, keylen: str, **extra_args: str) -> None: ensure_build_directory(args, ctx.info_name) generate_flash_encryption_key_args = [PYTHON, '-m', 'espsecure', 'generate_flash_encryption_key'] if keylen: generate_flash_encryption_key_args += ['--keylen', keylen] if extra_args['keyfile']: generate_flash_encryption_key_args += [extra_args['keyfile']] RunTool('espsecure', generate_flash_encryption_key_args, args.build_dir)() def secure_generate_signing_key(action: str, ctx: click.core.Context, args: PropertyDict, version: str, scheme: str, **extra_args: str) -> None: ensure_build_directory(args, ctx.info_name) generate_signing_key_args = [PYTHON, '-m', 'espsecure', 'generate_signing_key'] project_desc = _get_project_desc(ctx, args) ecdsa_scheme = get_sdkconfig_value(project_desc['config_file'], 'CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME') ecdsa_v2_scheme = get_sdkconfig_value(project_desc['config_file'], 'CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME') rsa_scheme = get_sdkconfig_value(project_desc['config_file'], 'CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME') if ecdsa_scheme: version = '1' elif ecdsa_v2_scheme or rsa_scheme: version = '2' if version: generate_signing_key_args += ['--version', version] if scheme: generate_signing_key_args += ['--scheme', '2'] if extra_args['keyfile']: generate_signing_key_args += [extra_args['keyfile']] RunTool('espsecure', generate_signing_key_args, args.build_dir)() def secure_sign_data(action: str, ctx: click.core.Context, args: PropertyDict, version: str, keyfile: str, append_signatures: bool, pub_key: str, signature: str, output: str, **extra_args: str) -> None: ensure_build_directory(args, ctx.info_name) sign_data_args = [PYTHON, '-m', 'espsecure', 'sign_data'] if version: sign_data_args += ['--version', version] if keyfile: sign_data_args += ['--keyfile', keyfile] if append_signatures: sign_data_args += ['--append_signatures'] if pub_key: sign_data_args += ['--pub-key', pub_key] if signature: sign_data_args += ['--signature', signature] if output: sign_data_args += ['--output', output] if extra_args['datafile']: sign_data_args += [extra_args['datafile']] RunTool('espsecure', sign_data_args, args.build_dir)() BAUD_AND_PORT = [BAUD_RATE, PORT] flash_options = BAUD_AND_PORT + [ { 'names': ['--force'], 'is_flag': True, 'help': 'Force write, skip security and compatibility checks. Use with caution!', }, { 'names': ['--extra-args'], 'help': ( 'Pass extra arguments to esptool separated by space. For more details see `esptool.py write_flash --help`. ' 'For example to compress and verify data use: `idf.py flash --extra-args="--compress --verify"`. Use with caution!' ) } ] serial_actions = { 'global_action_callbacks': [global_callback], 'actions': { 'flash': { 'callback': flash, 'help': 'Flash the project.', 'options': global_options + flash_options, 'order_dependencies': ['all', 'erase-flash'], }, 'erase-flash': { 'callback': erase_flash, 'help': 'Erase entire flash chip.', 'options': BAUD_AND_PORT, }, 'erase_flash': { 'callback': erase_flash, 'deprecated': { 'since': 'v4.4', 'removed': 'next major release', 'message': 'Have you wanted to run "erase-flash" instead?', }, 'hidden': True, 'help': 'Erase entire flash chip.', 'options': BAUD_AND_PORT, }, 'merge-bin': { 'callback': merge_bin, 'options': [ { 'names': ['--output', '-o'], 'help': ('Output filename'), 'type': click.Path(), }, { 'names': ['--format', '-f'], 'help': ('Format of the output file'), 'type': click.Choice(['hex', 'uf2', 'raw']), 'default': 'raw', }, { 'names': ['--md5-disable'], 'is_flag': True, 'help': ('[ONLY UF2] Disable MD5 checksum in UF2 output.'), }, { 'names': ['--flash-offset', '-t'], 'help': ('[ONLY RAW] Flash offset where the output file will be flashed.'), }, { 'names': ['--fill-flash-size'], 'help': ('[ONLY RAW] If set, the final binary file will be padded with FF bytes up to this flash size.'), 'type': click.Choice(['256KB', '512KB', '1MB', '2MB', '4MB', '8MB', '16MB', '32MB', '64MB', '128MB']), }, ], 'dependencies': ['all'], # all = build }, 'secure-decrypt-flash-data': { 'callback': secure_decrypt_flash_data, 'options': [ { 'names': ['--aes-xts', '-x'], 'is_flag': True, 'help': ('Decrypt data using AES-XTS.'), }, { 'names': ['--keyfile', '-k'], 'help': ('File with flash encryption key.'), }, { 'names': ['--output', '-o'], 'help': ('Output file for plaintext data.'), }, { 'names': ['--address', '-a'], 'help': ('Address offset in flash that file was read from.'), }, { 'names': ['--flash-crypt-conf'], 'help': ('Override FLASH_CRYPT_CONF efuse value (default is 0XF).'), }, ], 'arguments': [ { 'names': ['encrypted_file'], 'nargs': 1, }, ], }, 'secure-digest-secure-bootloader': { 'callback': secure_digest_secure_bootloader, 'help': ('Take a bootloader binary image and a secure boot key, and output a combined' 'digest+binary suitable for flashing along with the precalculated secure boot key.'), 'options': [ { 'names': ['--keyfile', '-k'], 'help': ('256 bit key for secure boot digest.'), }, { 'names': ['--output', '-o'], 'help': ('Output file for signed digest image.'), }, { 'names': ['--iv'], 'help': ( '128 byte IV file. Supply a file for testing purposes only, if not supplied an IV will be randomly generated.' ), }, ], 'arguments': [ { 'names': ['image'], 'nargs': 1, }, ], }, 'secure-encrypt-flash-data': { 'callback': secure_encrypt_flash_data, 'help': 'Encrypt some data suitable for encrypted flash (using known key).', 'options': [ { 'names': ['--aes-xts', '-x'], 'is_flag': True, 'help': ( 'Encrypt data using AES-XTS if chip supports it.' ), }, { 'names': ['--keyfile', '-k'], 'help': ('File with flash encryption key.'), }, { 'names': ['--output', '-o'], 'help': ('Output file for encrypted data.'), }, { 'names': ['--address', '-a'], 'help': ('Address offset in flash where file will be flashed.'), }, { 'names': ['--flash-crypt-conf'], 'help': ( 'Override FLASH_CRYPT_CONF eFuse value (default is 0XF).' ), }, ], 'arguments': [ { 'names': ['plaintext_file'], 'nargs': 1, }, ], }, 'secure-generate-flash-encryption-key': { 'callback': secure_generate_flash_encryption_key, 'options': [ { 'names': ['--keylen', '-l'], 'help': ( 'Length of private key digest file to generate (in bits). 3/4 Coding Scheme requires 192 bit key.' ), }, ], 'arguments': [ { 'names': ['keyfile'], 'nargs': 1, }, ], }, 'secure-generate-signing-key': { 'callback': secure_generate_signing_key, 'help': ('Generate a private key for signing secure boot images as per the secure boot version. Key file is generated in PEM' 'format, Secure Boot V1 - ECDSA NIST256p private key. Secure Boot V2 - RSA 3072, ECDSA NIST256p, ECDSA NIST192p' 'private key.'), 'options': [ { 'names': ['--version', '-v'], 'help': ('Version of the secure boot signing scheme to use.'), 'type': click.Choice(['1', '2']), 'default': '2', }, { 'names': ['--scheme', '-s'], 'help': ('Scheme of secure boot signing.'), 'type': click.Choice(['rsa3072', 'ecdsa192', 'ecdsa256']), }, ], 'arguments': [ { 'names': ['keyfile'], 'nargs': 1, }, ], }, 'secure-sign-data': { 'callback': secure_sign_data, 'help': ('Sign a data file for use with secure boot. Signing algorithm is deterministic ECDSA w/ SHA-512 (V1) or either RSA-' 'PSS or ECDSA w/ SHA-256 (V2).'), 'options': [ { 'names': ['--version', '-v'], 'help': ('Version of the secure boot signing scheme to use.'), 'type': click.Choice(['1', '2']), 'default': '2', }, { 'names': ['--keyfile', '-k'], 'help': ('Private key file for signing. Key is in PEM format.'), }, { 'names': ['--append-signatures', '-a'], 'is_flag': True, 'help': ( 'Append signature block(s) to already signed image. Valid only for ESP32-S2.' ), }, { 'names': ['--pub-key'], 'help': ( 'Public key files corresponding to the private key used to generate the pre-calculated signatures. Keys should be in PEM format.' ), }, { 'names': ['--signature'], 'help': ( 'Pre-calculated signatures. Signatures generated using external private keys e.g. keys stored in HSM.' ), }, { 'names': ['--output', '-o'], 'help': ( 'Output file for signed digest image. Default is to sign the input file.' ), }, ], 'arguments': [ { 'names': ['datafile'], 'nargs': 1, }, ], }, 'monitor': { 'callback': monitor, 'help': 'Display serial output.', 'options': [ PORT, { 'names': ['--print-filter', '--print_filter'], 'help': ('Filter monitor output. ' 'Restrictions on what to print can be specified as a series of : items ' 'where is the tag string and is a character from the set ' '{N, E, W, I, D, V, *} referring to a level. ' 'For example, "tag1:W" matches and prints only the outputs written with ' 'ESP_LOGW("tag1", ...) or at lower verbosity level, i.e. ESP_LOGE("tag1", ...). ' 'Not specifying a or using "*" defaults to Verbose level. ' 'Please see the IDF Monitor section of the ESP-IDF documentation ' 'for a more detailed description and further examples.'), 'default': None, }, { 'names': ['--monitor-baud', '-b'], 'type': click.INT, 'help': ('Baud rate for monitor. ' 'If this option is not provided IDF_MONITOR_BAUD and MONITORBAUD ' 'environment variables, global baud rate and project_description.json in build directory ' "(generated by CMake from project's sdkconfig) " 'will be checked for default value.'), }, { 'names': ['--encrypted', '-E'], 'is_flag': True, 'help': ('Enable encrypted flash targets. ' 'IDF Monitor will invoke encrypted-flash and encrypted-app-flash targets ' 'if this option is set. This option is set by default if IDF Monitor was invoked ' 'together with encrypted-flash or encrypted-app-flash target.'), }, { 'names': ['--no-reset'], 'is_flag': True, 'help': ('Disable reset on monitor startup. ' 'IDF Monitor will not reset the MCU target by toggling DTR/RTS lines on startup ' 'if this option is set.'), }, { 'names': ['--timestamps'], 'is_flag': True, 'help': 'Print a time stamp in the beginning of each line.', }, { 'names': ['--timestamp-format'], 'help': ('Set the formatting of timestamps compatible with strftime(). ' 'For example, "%Y-%m-%d %H:%M:%S".'), 'default': None }, { 'names': ['--force-color'], 'is_flag': True, 'help': 'Always print ANSI for colors', } ], 'order_dependencies': [ 'flash', 'encrypted-flash', 'partition-table-flash', 'bootloader-flash', 'app-flash', 'encrypted-app-flash', ], }, 'partition-table-flash': { 'callback': flash, 'help': 'Flash partition table only.', 'options': flash_options, 'order_dependencies': ['partition-table', 'erase-flash'], }, 'bootloader-flash': { 'callback': flash, 'help': 'Flash bootloader only.', 'options': flash_options, 'order_dependencies': ['bootloader', 'erase-flash'], }, 'app-flash': { 'callback': flash, 'help': 'Flash the app only.', 'options': flash_options, 'order_dependencies': ['app', 'erase-flash'], }, 'encrypted-app-flash': { 'callback': flash, 'help': 'Flash the encrypted app only.', 'options': flash_options, 'order_dependencies': ['app', 'erase-flash'], }, 'encrypted-flash': { 'callback': flash, 'help': 'Flash the encrypted project.', 'options': flash_options, 'order_dependencies': ['all', 'erase-flash'], }, 'erase-otadata': { 'callback': ota_targets, 'help': 'Erase otadata partition.', 'options': global_options + BAUD_AND_PORT, }, 'read-otadata': { 'callback': ota_targets, 'help': 'Read otadata partition.', 'options': global_options + BAUD_AND_PORT, }, }, } return serial_actions