diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..96bff3b
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,63 @@
+name: Build Release
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ target:
+ - "ttgo-lora32-v21"
+ - "heltec-lora32-v2"
+ - "heltec_wifi_lora_32_V3"
+ - "ESP32_DIY_LoRa"
+ - "ESP32_DIY_1W_LoRa"
+ - "ttgo-t-beam-v1_2"
+ - "ttgo-t-beam-v1"
+ - "ttgo-t-beam-v1_SX1268"
+ - "ttgo-t-beam-v1_2_SX1262"
+ # - "heltec_wireless_stick_lite" # NOT FULLY TESTED
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.9"
+
+ - name: Install PlatformIO Core
+ run: pip install --upgrade platformio
+
+ - name: Build target
+ run: pio run -e ${{ matrix.target }}
+
+ - name: Build FS
+ run: pio run --target buildfs -e ${{ matrix.target }}
+
+ - name: Move Files
+ run: |
+ mkdir -p installer/firmware
+ cp .pio/build/${{ matrix.target }}/firmware.bin installer/firmware/
+ cp .pio/build/${{ matrix.target }}/bootloader.bin installer/firmware/
+ cp .pio/build/${{ matrix.target }}/partitions.bin installer/firmware/
+ cp .pio/build/${{ matrix.target }}/spiffs.bin installer/firmware/
+ cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin installer/firmware/
+
+ - name: Install Zip
+ run: sudo apt-get install zip
+
+ - name: Archive Files
+ run: zip -r installer.zip installer/
+
+ - name: Upload Release Asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ github.event.release.upload_url }}
+ asset_path: ./installer.zip
+ asset_name: ${{ matrix.target }}.zip
+ asset_content_type: application/zip
diff --git a/.gitignore b/.gitignore
index 4de822f..3d683e2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,7 @@
.vscode/launch.json
.vscode/ipch
.DS_Store
+/data_embed/*.gz
+installer/firmware
+installer/*.bin
+**/__pycache__/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f179091
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "editor.tabSize": 4,
+ "editor.formatOnSave": true
+}
diff --git a/installer/bin/esptool/esptool.py b/installer/bin/esptool/esptool.py
new file mode 100644
index 0000000..60e5bd3
--- /dev/null
+++ b/installer/bin/esptool/esptool.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+#
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# This executable script is a thin wrapper around the main functionality
+# in the esptool Python package
+
+# When updating this script, please also update espefuse.py and espsecure.py
+
+import contextlib
+import os
+import sys
+
+if os.name != "nt":
+ # Linux/macOS: remove current script directory to avoid importing this file
+ # as a module; we want to import the installed esptool module instead
+ with contextlib.suppress(ValueError):
+ if sys.path[0].endswith("/bin"):
+ sys.path.pop(0)
+ sys.path.remove(os.path.dirname(sys.executable))
+
+ # Linux/macOS: delete imported module entry to force Python to load
+ # the module from scratch; this enables importing esptool module in
+ # other Python scripts
+ with contextlib.suppress(KeyError):
+ del sys.modules["esptool"]
+
+import esptool
+
+if __name__ == "__main__":
+ esptool._main()
diff --git a/installer/bin/esptool/esptool/__init__.py b/installer/bin/esptool/esptool/__init__.py
new file mode 100644
index 0000000..24272f2
--- /dev/null
+++ b/installer/bin/esptool/esptool/__init__.py
@@ -0,0 +1,1055 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+__all__ = [
+ "chip_id",
+ "detect_chip",
+ "dump_mem",
+ "elf2image",
+ "erase_flash",
+ "erase_region",
+ "flash_id",
+ "get_security_info",
+ "image_info",
+ "load_ram",
+ "make_image",
+ "merge_bin",
+ "read_flash",
+ "read_flash_status",
+ "read_mac",
+ "read_mem",
+ "run",
+ "verify_flash",
+ "version",
+ "write_flash",
+ "write_flash_status",
+ "write_mem",
+]
+
+__version__ = "4.5.1"
+
+import argparse
+import inspect
+import os
+import shlex
+import sys
+import time
+import traceback
+
+from esptool.cmds import (
+ chip_id,
+ detect_chip,
+ detect_flash_size,
+ dump_mem,
+ elf2image,
+ erase_flash,
+ erase_region,
+ flash_id,
+ get_security_info,
+ image_info,
+ load_ram,
+ make_image,
+ merge_bin,
+ read_flash,
+ read_flash_status,
+ read_mac,
+ read_mem,
+ run,
+ verify_flash,
+ version,
+ write_flash,
+ write_flash_status,
+ write_mem,
+)
+from esptool.config import load_config_file
+from esptool.loader import DEFAULT_CONNECT_ATTEMPTS, ESPLoader, list_ports
+from esptool.targets import CHIP_DEFS, CHIP_LIST, ESP32ROM
+from esptool.util import (
+ FatalError,
+ NotImplementedInROMError,
+ flash_size_bytes,
+ strip_chip_name,
+)
+
+import serial
+
+
+def main(argv=None, esp=None):
+ """
+ Main function for esptool
+
+ argv - Optional override for default arguments parsing (that uses sys.argv),
+ can be a list of custom arguments as strings. Arguments and their values
+ need to be added as individual items to the list
+ e.g. "-b 115200" thus becomes ['-b', '115200'].
+
+ esp - Optional override of the connected device previously
+ returned by get_default_connected_device()
+ """
+
+ external_esp = esp is not None
+
+ parser = argparse.ArgumentParser(
+ description="esptool.py v%s - Espressif chips ROM Bootloader Utility"
+ % __version__,
+ prog="esptool",
+ )
+
+ parser.add_argument(
+ "--chip",
+ "-c",
+ help="Target chip type",
+ type=strip_chip_name,
+ choices=["auto"] + CHIP_LIST,
+ default=os.environ.get("ESPTOOL_CHIP", "auto"),
+ )
+
+ parser.add_argument(
+ "--port",
+ "-p",
+ help="Serial port device",
+ default=os.environ.get("ESPTOOL_PORT", None),
+ )
+
+ parser.add_argument(
+ "--baud",
+ "-b",
+ help="Serial port baud rate used when flashing/reading",
+ type=arg_auto_int,
+ default=os.environ.get("ESPTOOL_BAUD", ESPLoader.ESP_ROM_BAUD),
+ )
+
+ parser.add_argument(
+ "--before",
+ help="What to do before connecting to the chip",
+ choices=["default_reset", "usb_reset", "no_reset", "no_reset_no_sync"],
+ default=os.environ.get("ESPTOOL_BEFORE", "default_reset"),
+ )
+
+ parser.add_argument(
+ "--after",
+ "-a",
+ help="What to do after esptool.py is finished",
+ choices=["hard_reset", "soft_reset", "no_reset", "no_reset_stub"],
+ default=os.environ.get("ESPTOOL_AFTER", "hard_reset"),
+ )
+
+ parser.add_argument(
+ "--no-stub",
+ help="Disable launching the flasher stub, only talk to ROM bootloader. "
+ "Some features will not be available.",
+ action="store_true",
+ )
+
+ parser.add_argument(
+ "--trace",
+ "-t",
+ help="Enable trace-level output of esptool.py interactions.",
+ action="store_true",
+ )
+
+ parser.add_argument(
+ "--override-vddsdio",
+ help="Override ESP32 VDDSDIO internal voltage regulator (use with care)",
+ choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES,
+ nargs="?",
+ )
+
+ parser.add_argument(
+ "--connect-attempts",
+ help=(
+ "Number of attempts to connect, negative or 0 for infinite. "
+ "Default: %d." % DEFAULT_CONNECT_ATTEMPTS
+ ),
+ type=int,
+ default=os.environ.get("ESPTOOL_CONNECT_ATTEMPTS", DEFAULT_CONNECT_ATTEMPTS),
+ )
+
+ subparsers = parser.add_subparsers(
+ dest="operation", help="Run esptool.py {command} -h for additional help"
+ )
+
+ def add_spi_connection_arg(parent):
+ parent.add_argument(
+ "--spi-connection",
+ "-sc",
+ help="ESP32-only argument. Override default SPI Flash connection. "
+ "Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers "
+ "to use for SPI flash (CLK,Q,D,HD,CS).",
+ action=SpiConnectionAction,
+ )
+
+ parser_load_ram = subparsers.add_parser(
+ "load_ram", help="Download an image to RAM and execute"
+ )
+ parser_load_ram.add_argument("filename", help="Firmware image")
+
+ parser_dump_mem = subparsers.add_parser(
+ "dump_mem", help="Dump arbitrary memory to disk"
+ )
+ parser_dump_mem.add_argument("address", help="Base address", type=arg_auto_int)
+ parser_dump_mem.add_argument(
+ "size", help="Size of region to dump", type=arg_auto_int
+ )
+ parser_dump_mem.add_argument("filename", help="Name of binary dump")
+
+ parser_read_mem = subparsers.add_parser(
+ "read_mem", help="Read arbitrary memory location"
+ )
+ parser_read_mem.add_argument("address", help="Address to read", type=arg_auto_int)
+
+ parser_write_mem = subparsers.add_parser(
+ "write_mem", help="Read-modify-write to arbitrary memory location"
+ )
+ parser_write_mem.add_argument("address", help="Address to write", type=arg_auto_int)
+ parser_write_mem.add_argument("value", help="Value", type=arg_auto_int)
+ parser_write_mem.add_argument(
+ "mask",
+ help="Mask of bits to write",
+ type=arg_auto_int,
+ nargs="?",
+ default="0xFFFFFFFF",
+ )
+
+ def add_spi_flash_subparsers(parent, allow_keep, auto_detect):
+ """Add common parser arguments for SPI flash properties"""
+ extra_keep_args = ["keep"] if allow_keep else []
+
+ if auto_detect and allow_keep:
+ extra_fs_message = ", detect, or keep"
+ flash_sizes = ["detect", "keep"]
+ elif auto_detect:
+ extra_fs_message = ", or detect"
+ flash_sizes = ["detect"]
+ elif allow_keep:
+ extra_fs_message = ", or keep"
+ flash_sizes = ["keep"]
+ else:
+ extra_fs_message = ""
+ flash_sizes = []
+
+ parent.add_argument(
+ "--flash_freq",
+ "-ff",
+ help="SPI Flash frequency",
+ choices=extra_keep_args
+ + [
+ "80m",
+ "60m",
+ "48m",
+ "40m",
+ "30m",
+ "26m",
+ "24m",
+ "20m",
+ "16m",
+ "15m",
+ "12m",
+ ],
+ default=os.environ.get("ESPTOOL_FF", "keep" if allow_keep else None),
+ )
+ parent.add_argument(
+ "--flash_mode",
+ "-fm",
+ help="SPI Flash mode",
+ choices=extra_keep_args + ["qio", "qout", "dio", "dout"],
+ default=os.environ.get("ESPTOOL_FM", "keep" if allow_keep else "qio"),
+ )
+ parent.add_argument(
+ "--flash_size",
+ "-fs",
+ help="SPI Flash size in MegaBytes "
+ "(1MB, 2MB, 4MB, 8MB, 16MB, 32MB, 64MB, 128MB) "
+ "plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)" + extra_fs_message,
+ choices=flash_sizes
+ + [
+ "256KB",
+ "512KB",
+ "1MB",
+ "2MB",
+ "2MB-c1",
+ "4MB",
+ "4MB-c1",
+ "8MB",
+ "16MB",
+ "32MB",
+ "64MB",
+ "128MB",
+ ],
+ default=os.environ.get("ESPTOOL_FS", "keep" if allow_keep else "1MB"),
+ )
+ add_spi_connection_arg(parent)
+
+ parser_write_flash = subparsers.add_parser(
+ "write_flash", help="Write a binary blob to flash"
+ )
+
+ parser_write_flash.add_argument(
+ "addr_filename",
+ metavar="
",
+ help="Address followed by binary filename, separated by space",
+ action=AddrFilenamePairAction,
+ )
+ parser_write_flash.add_argument(
+ "--erase-all",
+ "-e",
+ help="Erase all regions of flash (not just write areas) before programming",
+ action="store_true",
+ )
+
+ add_spi_flash_subparsers(parser_write_flash, allow_keep=True, auto_detect=True)
+ parser_write_flash.add_argument(
+ "--no-progress", "-p", help="Suppress progress output", action="store_true"
+ )
+ parser_write_flash.add_argument(
+ "--verify",
+ help="Verify just-written data on flash "
+ "(mostly superfluous, data is read back during flashing)",
+ action="store_true",
+ )
+ parser_write_flash.add_argument(
+ "--encrypt",
+ help="Apply flash encryption when writing data "
+ "(required correct efuse settings)",
+ action="store_true",
+ )
+ # In order to not break backward compatibility,
+ # our list of encrypted files to flash is a new parameter
+ parser_write_flash.add_argument(
+ "--encrypt-files",
+ metavar=" ",
+ help="Files to be encrypted on the flash. "
+ "Address followed by binary filename, separated by space.",
+ action=AddrFilenamePairAction,
+ )
+ parser_write_flash.add_argument(
+ "--ignore-flash-encryption-efuse-setting",
+ help="Ignore flash encryption efuse settings ",
+ action="store_true",
+ )
+ parser_write_flash.add_argument(
+ "--force",
+ help="Force write, skip security and compatibility checks. Use with caution!",
+ action="store_true",
+ )
+
+ compress_args = parser_write_flash.add_mutually_exclusive_group(required=False)
+ compress_args.add_argument(
+ "--compress",
+ "-z",
+ help="Compress data in transfer (default unless --no-stub is specified)",
+ action="store_true",
+ default=None,
+ )
+ compress_args.add_argument(
+ "--no-compress",
+ "-u",
+ help="Disable data compression during transfer "
+ "(default if --no-stub is specified)",
+ action="store_true",
+ )
+
+ subparsers.add_parser("run", help="Run application code in flash")
+
+ parser_image_info = subparsers.add_parser(
+ "image_info", help="Dump headers from an application image"
+ )
+ parser_image_info.add_argument("filename", help="Image file to parse")
+ parser_image_info.add_argument(
+ "--version",
+ "-v",
+ help="Output format version (1 - legacy, 2 - extended)",
+ choices=["1", "2"],
+ default="1",
+ )
+
+ parser_make_image = subparsers.add_parser(
+ "make_image", help="Create an application image from binary files"
+ )
+ parser_make_image.add_argument("output", help="Output image file")
+ parser_make_image.add_argument(
+ "--segfile", "-f", action="append", help="Segment input file"
+ )
+ parser_make_image.add_argument(
+ "--segaddr",
+ "-a",
+ action="append",
+ help="Segment base address",
+ type=arg_auto_int,
+ )
+ parser_make_image.add_argument(
+ "--entrypoint",
+ "-e",
+ help="Address of entry point",
+ type=arg_auto_int,
+ default=0,
+ )
+
+ parser_elf2image = subparsers.add_parser(
+ "elf2image", help="Create an application image from ELF file"
+ )
+ parser_elf2image.add_argument("input", help="Input ELF file")
+ parser_elf2image.add_argument(
+ "--output",
+ "-o",
+ help="Output filename prefix (for version 1 image), "
+ "or filename (for version 2 single image)",
+ type=str,
+ )
+ parser_elf2image.add_argument(
+ "--version",
+ "-e",
+ help="Output image version",
+ choices=["1", "2", "3"],
+ default="1",
+ )
+ parser_elf2image.add_argument(
+ # it kept for compatibility
+ # Minimum chip revision (deprecated, consider using --min-rev-full)
+ "--min-rev",
+ "-r",
+ help=argparse.SUPPRESS,
+ type=int,
+ choices=range(256),
+ metavar="{0, ... 255}",
+ default=0,
+ )
+ parser_elf2image.add_argument(
+ "--min-rev-full",
+ help="Minimal chip revision (in format: major * 100 + minor)",
+ type=int,
+ choices=range(65536),
+ metavar="{0, ... 65535}",
+ default=0,
+ )
+ parser_elf2image.add_argument(
+ "--max-rev-full",
+ help="Maximal chip revision (in format: major * 100 + minor)",
+ type=int,
+ choices=range(65536),
+ metavar="{0, ... 65535}",
+ default=65535,
+ )
+ parser_elf2image.add_argument(
+ "--secure-pad",
+ action="store_true",
+ help="Pad image so once signed it will end on a 64KB boundary. "
+ "For Secure Boot v1 images only.",
+ )
+ parser_elf2image.add_argument(
+ "--secure-pad-v2",
+ action="store_true",
+ help="Pad image to 64KB, so once signed its signature sector will"
+ "start at the next 64K block. For Secure Boot v2 images only.",
+ )
+ parser_elf2image.add_argument(
+ "--elf-sha256-offset",
+ help="If set, insert SHA256 hash (32 bytes) of the input ELF file "
+ "at specified offset in the binary.",
+ type=arg_auto_int,
+ default=None,
+ )
+ parser_elf2image.add_argument(
+ "--dont-append-digest",
+ dest="append_digest",
+ help="Don't append a SHA256 digest of the entire image after the checksum. "
+ "This argument is not supported and ignored for ESP8266.",
+ action="store_false",
+ default=True,
+ )
+ parser_elf2image.add_argument(
+ "--use_segments",
+ help="If set, ELF segments will be used instead of ELF sections "
+ "to genereate the image.",
+ action="store_true",
+ )
+ parser_elf2image.add_argument(
+ "--flash-mmu-page-size",
+ help="Change flash MMU page size.",
+ choices=["64KB", "32KB", "16KB", "8KB"],
+ )
+ parser_elf2image.add_argument(
+ "--pad-to-size",
+ help="The block size with which the final binary image after padding "
+ "must be aligned to. Value 0xFF is used for padding, similar to erase_flash",
+ default=None,
+ )
+
+ add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False)
+
+ subparsers.add_parser("read_mac", help="Read MAC address from OTP ROM")
+
+ subparsers.add_parser("chip_id", help="Read Chip ID from OTP ROM")
+
+ parser_flash_id = subparsers.add_parser(
+ "flash_id", help="Read SPI flash manufacturer and device ID"
+ )
+ add_spi_connection_arg(parser_flash_id)
+
+ parser_read_status = subparsers.add_parser(
+ "read_flash_status", help="Read SPI flash status register"
+ )
+
+ add_spi_connection_arg(parser_read_status)
+ parser_read_status.add_argument(
+ "--bytes",
+ help="Number of bytes to read (1-3)",
+ type=int,
+ choices=[1, 2, 3],
+ default=2,
+ )
+
+ parser_write_status = subparsers.add_parser(
+ "write_flash_status", help="Write SPI flash status register"
+ )
+
+ add_spi_connection_arg(parser_write_status)
+ parser_write_status.add_argument(
+ "--non-volatile",
+ help="Write non-volatile bits (use with caution)",
+ action="store_true",
+ )
+ parser_write_status.add_argument(
+ "--bytes",
+ help="Number of status bytes to write (1-3)",
+ type=int,
+ choices=[1, 2, 3],
+ default=2,
+ )
+ parser_write_status.add_argument("value", help="New value", type=arg_auto_int)
+
+ parser_read_flash = subparsers.add_parser(
+ "read_flash", help="Read SPI flash content"
+ )
+ add_spi_connection_arg(parser_read_flash)
+ parser_read_flash.add_argument("address", help="Start address", type=arg_auto_int)
+ parser_read_flash.add_argument(
+ "size", help="Size of region to dump", type=arg_auto_int
+ )
+ parser_read_flash.add_argument("filename", help="Name of binary dump")
+ parser_read_flash.add_argument(
+ "--no-progress", "-p", help="Suppress progress output", action="store_true"
+ )
+
+ parser_verify_flash = subparsers.add_parser(
+ "verify_flash", help="Verify a binary blob against flash"
+ )
+ parser_verify_flash.add_argument(
+ "addr_filename",
+ help="Address and binary file to verify there, separated by space",
+ action=AddrFilenamePairAction,
+ )
+ parser_verify_flash.add_argument(
+ "--diff", "-d", help="Show differences", choices=["no", "yes"], default="no"
+ )
+ add_spi_flash_subparsers(parser_verify_flash, allow_keep=True, auto_detect=True)
+
+ parser_erase_flash = subparsers.add_parser(
+ "erase_flash", help="Perform Chip Erase on SPI flash"
+ )
+ parser_erase_flash.add_argument(
+ "--force",
+ help="Erase flash even if security features are enabled. Use with caution!",
+ action="store_true",
+ )
+ add_spi_connection_arg(parser_erase_flash)
+
+ parser_erase_region = subparsers.add_parser(
+ "erase_region", help="Erase a region of the flash"
+ )
+ parser_erase_region.add_argument(
+ "--force",
+ help="Erase region even if security features are enabled. Use with caution!",
+ action="store_true",
+ )
+ add_spi_connection_arg(parser_erase_region)
+ parser_erase_region.add_argument(
+ "address", help="Start address (must be multiple of 4096)", type=arg_auto_int
+ )
+ parser_erase_region.add_argument(
+ "size",
+ help="Size of region to erase (must be multiple of 4096)",
+ type=arg_auto_int,
+ )
+
+ parser_merge_bin = subparsers.add_parser(
+ "merge_bin",
+ help="Merge multiple raw binary files into a single file for later flashing",
+ )
+
+ parser_merge_bin.add_argument(
+ "--output", "-o", help="Output filename", type=str, required=True
+ )
+ parser_merge_bin.add_argument(
+ "--format", "-f", help="Format of the output file", choices="raw", default="raw"
+ ) # for future expansion
+ add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False)
+
+ parser_merge_bin.add_argument(
+ "--target-offset",
+ "-t",
+ help="Target offset where the output file will be flashed",
+ type=arg_auto_int,
+ default=0,
+ )
+ parser_merge_bin.add_argument(
+ "--fill-flash-size",
+ help="If set, the final binary file will be padded with FF "
+ "bytes up to this flash size.",
+ choices=[
+ "256KB",
+ "512KB",
+ "1MB",
+ "2MB",
+ "4MB",
+ "8MB",
+ "16MB",
+ "32MB",
+ "64MB",
+ "128MB",
+ ],
+ )
+ parser_merge_bin.add_argument(
+ "addr_filename",
+ metavar=" ",
+ help="Address followed by binary filename, separated by space",
+ action=AddrFilenamePairAction,
+ )
+
+ subparsers.add_parser("get_security_info", help="Get some security-related data")
+
+ subparsers.add_parser("version", help="Print esptool version")
+
+ # internal sanity check - every operation matches a module function of the same name
+ for operation in subparsers.choices.keys():
+ assert operation in globals(), "%s should be a module function" % operation
+
+ argv = expand_file_arguments(argv or sys.argv[1:])
+
+ args = parser.parse_args(argv)
+ print("esptool.py v%s" % __version__)
+ load_config_file(verbose=True)
+
+ # operation function can take 1 arg (args), 2 args (esp, arg)
+ # or be a member function of the ESPLoader class.
+
+ if args.operation is None:
+ parser.print_help()
+ sys.exit(1)
+
+ # Forbid the usage of both --encrypt, which means encrypt all the given files,
+ # and --encrypt-files, which represents the list of files to encrypt.
+ # The reason is that allowing both at the same time increases the chances of
+ # having contradictory lists (e.g. one file not available in one of list).
+ if (
+ args.operation == "write_flash"
+ and args.encrypt
+ and args.encrypt_files is not None
+ ):
+ raise FatalError(
+ "Options --encrypt and --encrypt-files "
+ "must not be specified at the same time."
+ )
+
+ operation_func = globals()[args.operation]
+ operation_args = inspect.getfullargspec(operation_func).args
+
+ if (
+ operation_args[0] == "esp"
+ ): # operation function takes an ESPLoader connection object
+ if args.before != "no_reset_no_sync":
+ initial_baud = min(
+ ESPLoader.ESP_ROM_BAUD, args.baud
+ ) # don't sync faster than the default baud rate
+ else:
+ initial_baud = args.baud
+
+ if args.port is None:
+ ser_list = get_port_list()
+ print("Found %d serial ports" % len(ser_list))
+ else:
+ ser_list = [args.port]
+ esp = esp or get_default_connected_device(
+ ser_list,
+ port=args.port,
+ connect_attempts=args.connect_attempts,
+ initial_baud=initial_baud,
+ chip=args.chip,
+ trace=args.trace,
+ before=args.before,
+ )
+
+ if esp is None:
+ raise FatalError(
+ "Could not connect to an Espressif device "
+ "on any of the %d available serial ports." % len(ser_list)
+ )
+
+ if esp.secure_download_mode:
+ print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME)
+ else:
+ print("Chip is %s" % (esp.get_chip_description()))
+ print("Features: %s" % ", ".join(esp.get_chip_features()))
+ print("Crystal is %dMHz" % esp.get_crystal_freq())
+ read_mac(esp, args)
+
+ if not args.no_stub:
+ if esp.secure_download_mode:
+ print(
+ "WARNING: Stub loader is not supported in Secure Download Mode, "
+ "setting --no-stub"
+ )
+ args.no_stub = True
+ elif not esp.IS_STUB and esp.stub_is_disabled:
+ print(
+ "WARNING: Stub loader has been disabled for compatibility, "
+ "setting --no-stub"
+ )
+ args.no_stub = True
+ else:
+ esp = esp.run_stub()
+
+ if args.override_vddsdio:
+ esp.override_vddsdio(args.override_vddsdio)
+
+ if args.baud > initial_baud:
+ try:
+ esp.change_baud(args.baud)
+ except NotImplementedInROMError:
+ print(
+ "WARNING: ROM doesn't support changing baud rate. "
+ "Keeping initial baud rate %d" % initial_baud
+ )
+
+ # override common SPI flash parameter stuff if configured to do so
+ if hasattr(args, "spi_connection") and args.spi_connection is not None:
+ if esp.CHIP_NAME != "ESP32":
+ raise FatalError(
+ "Chip %s does not support --spi-connection option." % esp.CHIP_NAME
+ )
+ print("Configuring SPI flash mode...")
+ esp.flash_spi_attach(args.spi_connection)
+ elif args.no_stub:
+ print("Enabling default SPI flash mode...")
+ # ROM loader doesn't enable flash unless we explicitly do it
+ esp.flash_spi_attach(0)
+
+ # XMC chip startup sequence
+ XMC_VENDOR_ID = 0x20
+
+ def is_xmc_chip_strict():
+ id = esp.flash_id()
+ rdid = ((id & 0xFF) << 16) | ((id >> 16) & 0xFF) | (id & 0xFF00)
+
+ vendor_id = (rdid >> 16) & 0xFF
+ mfid = (rdid >> 8) & 0xFF
+ cpid = rdid & 0xFF
+
+ if vendor_id != XMC_VENDOR_ID:
+ return False
+
+ matched = False
+ if mfid == 0x40:
+ if cpid >= 0x13 and cpid <= 0x20:
+ matched = True
+ elif mfid == 0x41:
+ if cpid >= 0x17 and cpid <= 0x20:
+ matched = True
+ elif mfid == 0x50:
+ if cpid >= 0x15 and cpid <= 0x16:
+ matched = True
+ return matched
+
+ def flash_xmc_startup():
+ # If the RDID value is a valid XMC one, may skip the flow
+ fast_check = True
+ if fast_check and is_xmc_chip_strict():
+ return # Successful XMC flash chip boot-up detected by RDID, skipping.
+
+ sfdp_mfid_addr = 0x10
+ mf_id = esp.read_spiflash_sfdp(sfdp_mfid_addr, 8)
+ if mf_id != XMC_VENDOR_ID: # Non-XMC chip detected by SFDP Read, skipping.
+ return
+
+ print(
+ "WARNING: XMC flash chip boot-up failure detected! "
+ "Running XMC25QHxxC startup flow"
+ )
+ esp.run_spiflash_command(0xB9) # Enter DPD
+ esp.run_spiflash_command(0x79) # Enter UDPD
+ esp.run_spiflash_command(0xFF) # Exit UDPD
+ time.sleep(0.002) # Delay tXUDPD
+ esp.run_spiflash_command(0xAB) # Release Power-Down
+ time.sleep(0.00002)
+ # Check for success
+ if not is_xmc_chip_strict():
+ print("WARNING: XMC flash boot-up fix failed.")
+ print("XMC flash chip boot-up fix successful!")
+
+ # Check flash chip connection
+ if not esp.secure_download_mode:
+ try:
+ flash_id = esp.flash_id()
+ if flash_id in (0xFFFFFF, 0x000000):
+ print(
+ "WARNING: Failed to communicate with the flash chip, "
+ "read/write operations will fail. "
+ "Try checking the chip connections or removing "
+ "any other hardware connected to IOs."
+ )
+ except FatalError as e:
+ raise FatalError(f"Unable to verify flash chip connection ({e}).")
+
+ # Check if XMC SPI flash chip booted-up successfully, fix if not
+ if not esp.secure_download_mode:
+ try:
+ flash_xmc_startup()
+ except FatalError as e:
+ esp.trace(f"Unable to perform XMC flash chip startup sequence ({e}).")
+
+ if hasattr(args, "flash_size"):
+ print("Configuring flash size...")
+ detect_flash_size(esp, args)
+ if args.flash_size != "keep": # TODO: should set this even with 'keep'
+ esp.flash_set_parameters(flash_size_bytes(args.flash_size))
+ # Check if stub supports chosen flash size
+ if esp.IS_STUB and args.flash_size in ("32MB", "64MB", "128MB"):
+ print(
+ "WARNING: Flasher stub doesn't fully support flash size larger "
+ "than 16MB, in case of failure use --no-stub."
+ )
+
+ if esp.IS_STUB and hasattr(args, "address") and hasattr(args, "size"):
+ if args.address + args.size > 0x1000000:
+ print(
+ "WARNING: Flasher stub doesn't fully support flash size larger "
+ "than 16MB, in case of failure use --no-stub."
+ )
+
+ try:
+ operation_func(esp, args)
+ finally:
+ try: # Clean up AddrFilenamePairAction files
+ for address, argfile in args.addr_filename:
+ argfile.close()
+ except AttributeError:
+ pass
+
+ # Handle post-operation behaviour (reset or other)
+ if operation_func == load_ram:
+ # the ESP is now running the loaded image, so let it run
+ print("Exiting immediately.")
+ elif args.after == "hard_reset":
+ esp.hard_reset()
+ elif args.after == "soft_reset":
+ print("Soft resetting...")
+ # flash_finish will trigger a soft reset
+ esp.soft_reset(False)
+ elif args.after == "no_reset_stub":
+ print("Staying in flasher stub.")
+ else: # args.after == 'no_reset'
+ print("Staying in bootloader.")
+ if esp.IS_STUB:
+ esp.soft_reset(True) # exit stub back to ROM loader
+
+ if not external_esp:
+ esp._port.close()
+
+ else:
+ operation_func(args)
+
+
+def arg_auto_int(x):
+ return int(x, 0)
+
+
+def get_port_list():
+ if list_ports is None:
+ raise FatalError(
+ "Listing all serial ports is currently not available. "
+ "Please try to specify the port when running esptool.py or update "
+ "the pyserial package to the latest version"
+ )
+ return sorted(ports.device for ports in list_ports.comports())
+
+
+def expand_file_arguments(argv):
+ """
+ Any argument starting with "@" gets replaced with all values read from a text file.
+ Text file arguments can be split by newline or by space.
+ Values are added "as-is", as if they were specified in this order
+ on the command line.
+ """
+ new_args = []
+ expanded = False
+ for arg in argv:
+ if arg.startswith("@"):
+ expanded = True
+ with open(arg[1:], "r") as f:
+ for line in f.readlines():
+ new_args += shlex.split(line)
+ else:
+ new_args.append(arg)
+ if expanded:
+ print("esptool %s" % (" ".join(new_args[1:])))
+ return new_args
+ return argv
+
+
+def get_default_connected_device(
+ serial_list,
+ port,
+ connect_attempts,
+ initial_baud,
+ chip="auto",
+ trace=False,
+ before="default_reset",
+):
+ _esp = None
+ for each_port in reversed(serial_list):
+ print("Serial port %s" % each_port)
+ try:
+ if chip == "auto":
+ _esp = detect_chip(
+ each_port, initial_baud, before, trace, connect_attempts
+ )
+ else:
+ chip_class = CHIP_DEFS[chip]
+ _esp = chip_class(each_port, initial_baud, trace)
+ _esp.connect(before, connect_attempts)
+ break
+ except (FatalError, OSError) as err:
+ if port is not None:
+ raise
+ print("%s failed to connect: %s" % (each_port, err))
+ if _esp and _esp._port:
+ _esp._port.close()
+ _esp = None
+ return _esp
+
+
+class SpiConnectionAction(argparse.Action):
+ """
+ Custom action to parse 'spi connection' override.
+ Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas.
+ """
+
+ def __call__(self, parser, namespace, value, option_string=None):
+ if value.upper() == "SPI":
+ value = 0
+ elif value.upper() == "HSPI":
+ value = 1
+ elif "," in value:
+ values = value.split(",")
+ if len(values) != 5:
+ raise argparse.ArgumentError(
+ self,
+ "%s is not a valid list of comma-separate pin numbers. "
+ "Must be 5 numbers - CLK,Q,D,HD,CS." % value,
+ )
+ try:
+ values = tuple(int(v, 0) for v in values)
+ except ValueError:
+ raise argparse.ArgumentError(
+ self,
+ "%s is not a valid argument. All pins must be numeric values"
+ % values,
+ )
+ if any([v for v in values if v > 33 or v < 0]):
+ raise argparse.ArgumentError(
+ self, "Pin numbers must be in the range 0-33."
+ )
+ # encode the pin numbers as a 32-bit integer with packed 6-bit values,
+ # the same way ESP32 ROM takes them
+ # TODO: make this less ESP32 ROM specific somehow...
+ clk, q, d, hd, cs = values
+ value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk
+ else:
+ raise argparse.ArgumentError(
+ self,
+ "%s is not a valid spi-connection value. "
+ "Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS)."
+ % value,
+ )
+ setattr(namespace, self.dest, value)
+
+
+class AddrFilenamePairAction(argparse.Action):
+ """Custom parser class for the address/filename pairs passed as arguments"""
+
+ def __init__(self, option_strings, dest, nargs="+", **kwargs):
+ super(AddrFilenamePairAction, self).__init__(
+ option_strings, dest, nargs, **kwargs
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ # validate pair arguments
+ pairs = []
+ for i in range(0, len(values), 2):
+ try:
+ address = int(values[i], 0)
+ except ValueError:
+ raise argparse.ArgumentError(
+ self, 'Address "%s" must be a number' % values[i]
+ )
+ try:
+ argfile = open(values[i + 1], "rb")
+ except IOError as e:
+ raise argparse.ArgumentError(self, e)
+ except IndexError:
+ raise argparse.ArgumentError(
+ self,
+ "Must be pairs of an address "
+ "and the binary filename to write there",
+ )
+ pairs.append((address, argfile))
+
+ # Sort the addresses and check for overlapping
+ end = 0
+ for address, argfile in sorted(pairs, key=lambda x: x[0]):
+ argfile.seek(0, 2) # seek to end
+ size = argfile.tell()
+ argfile.seek(0)
+ sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)
+ sector_end = (
+ (address + size + ESPLoader.FLASH_SECTOR_SIZE - 1)
+ & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)
+ ) - 1
+ if sector_start < end:
+ message = "Detected overlap at address: 0x%x for file: %s" % (
+ address,
+ argfile.name,
+ )
+ raise argparse.ArgumentError(self, message)
+ end = sector_end
+ setattr(namespace, self.dest, pairs)
+
+
+def _main():
+ try:
+ main()
+ except FatalError as e:
+ print(f"\nA fatal error occurred: {e}")
+ sys.exit(2)
+ except serial.serialutil.SerialException as e:
+ print(f"\nA serial exception error occurred: {e}")
+ print(
+ "Note: This error originates from pySerial. "
+ "It is likely not a problem with esptool, "
+ "but with the hardware connection or drivers."
+ )
+ print(
+ "For troubleshooting steps visit: "
+ "https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html"
+ )
+ sys.exit(1)
+ except StopIteration:
+ print(traceback.format_exc())
+ print("A fatal error occurred: The chip stopped responding.")
+ sys.exit(2)
+
+
+if __name__ == "__main__":
+ _main()
diff --git a/installer/bin/esptool/esptool/__main__.py b/installer/bin/esptool/esptool/__main__.py
new file mode 100644
index 0000000..11e3bce
--- /dev/null
+++ b/installer/bin/esptool/esptool/__main__.py
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import esptool
+
+if __name__ == "__main__":
+ esptool._main()
diff --git a/installer/bin/esptool/esptool/bin_image.py b/installer/bin/esptool/esptool/bin_image.py
new file mode 100644
index 0000000..b001a1e
--- /dev/null
+++ b/installer/bin/esptool/esptool/bin_image.py
@@ -0,0 +1,1239 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import binascii
+import copy
+import hashlib
+import io
+import os
+import re
+import struct
+
+from .loader import ESPLoader
+from .targets import (
+ ESP32C2ROM,
+ ESP32C3ROM,
+ ESP32C6BETAROM,
+ ESP32C6ROM,
+ ESP32H2BETA1ROM,
+ ESP32H2BETA2ROM,
+ ESP32H2ROM,
+ ESP32ROM,
+ ESP32S2ROM,
+ ESP32S3BETA2ROM,
+ ESP32S3ROM,
+ ESP8266ROM,
+)
+from .util import FatalError, byte, pad_to
+
+
+def align_file_position(f, size):
+ """Align the position in the file to the next block of specified size"""
+ align = (size - 1) - (f.tell() % size)
+ f.seek(align, 1)
+
+
+def LoadFirmwareImage(chip, image_file):
+ """
+ Load a firmware image. Can be for any supported SoC.
+
+ ESP8266 images will be examined to determine if they are original ROM firmware
+ images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images.
+
+ Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1)
+ or ESP8266V2FirmwareImage (v2).
+ """
+
+ def select_image_class(f, chip):
+ chip = re.sub(r"[-()]", "", chip.lower())
+ if chip != "esp8266":
+ return {
+ "esp32": ESP32FirmwareImage,
+ "esp32s2": ESP32S2FirmwareImage,
+ "esp32s3beta2": ESP32S3BETA2FirmwareImage,
+ "esp32s3": ESP32S3FirmwareImage,
+ "esp32c3": ESP32C3FirmwareImage,
+ "esp32c6beta": ESP32C6BETAFirmwareImage,
+ "esp32h2beta1": ESP32H2BETA1FirmwareImage,
+ "esp32h2beta2": ESP32H2BETA2FirmwareImage,
+ "esp32c2": ESP32C2FirmwareImage,
+ "esp32c6": ESP32C6FirmwareImage,
+ "esp32h2": ESP32H2FirmwareImage,
+ }[chip](f)
+ else: # Otherwise, ESP8266 so look at magic to determine the image type
+ magic = ord(f.read(1))
+ f.seek(0)
+ if magic == ESPLoader.ESP_IMAGE_MAGIC:
+ return ESP8266ROMFirmwareImage(f)
+ elif magic == ESP8266V2FirmwareImage.IMAGE_V2_MAGIC:
+ return ESP8266V2FirmwareImage(f)
+ else:
+ raise FatalError("Invalid image magic number: %d" % magic)
+
+ if isinstance(image_file, str):
+ with open(image_file, "rb") as f:
+ return select_image_class(f, chip)
+ return select_image_class(image_file, chip)
+
+
+class ImageSegment(object):
+ """Wrapper class for a segment in an ESP image
+ (very similar to a section in an ELFImage also)"""
+
+ def __init__(self, addr, data, file_offs=None):
+ self.addr = addr
+ self.data = data
+ self.file_offs = file_offs
+ self.include_in_checksum = True
+ if self.addr != 0:
+ self.pad_to_alignment(
+ 4
+ ) # pad all "real" ImageSegments 4 byte aligned length
+
+ def copy_with_new_addr(self, new_addr):
+ """Return a new ImageSegment with same data, but mapped at
+ a new address."""
+ return ImageSegment(new_addr, self.data, 0)
+
+ def split_image(self, split_len):
+ """Return a new ImageSegment which splits "split_len" bytes
+ from the beginning of the data. Remaining bytes are kept in
+ this segment object (and the start address is adjusted to match.)"""
+ result = copy.copy(self)
+ result.data = self.data[:split_len]
+ self.data = self.data[split_len:]
+ self.addr += split_len
+ self.file_offs = None
+ result.file_offs = None
+ return result
+
+ def __repr__(self):
+ r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr)
+ if self.file_offs is not None:
+ r += " file_offs 0x%08x" % (self.file_offs)
+ return r
+
+ def get_memory_type(self, image):
+ """
+ Return a list describing the memory type(s) that is covered by this
+ segment's start address.
+ """
+ return [
+ map_range[2]
+ for map_range in image.ROM_LOADER.MEMORY_MAP
+ if map_range[0] <= self.addr < map_range[1]
+ ]
+
+ def pad_to_alignment(self, alignment):
+ self.data = pad_to(self.data, alignment, b"\x00")
+
+
+class ELFSection(ImageSegment):
+ """Wrapper class for a section in an ELF image, has a section
+ name as well as the common properties of an ImageSegment."""
+
+ def __init__(self, name, addr, data):
+ super(ELFSection, self).__init__(addr, data)
+ self.name = name.decode("utf-8")
+
+ def __repr__(self):
+ return "%s %s" % (self.name, super(ELFSection, self).__repr__())
+
+
+class BaseFirmwareImage(object):
+ SEG_HEADER_LEN = 8
+ SHA256_DIGEST_LEN = 32
+
+ """ Base class with common firmware image functions """
+
+ def __init__(self):
+ self.segments = []
+ self.entrypoint = 0
+ self.elf_sha256 = None
+ self.elf_sha256_offset = 0
+ self.pad_to_size = 0
+
+ def load_common_header(self, load_file, expected_magic):
+ (
+ magic,
+ segments,
+ self.flash_mode,
+ self.flash_size_freq,
+ self.entrypoint,
+ ) = struct.unpack(" 16:
+ raise FatalError(
+ "Invalid segment count %d (max 16). "
+ "Usually this indicates a linker script problem." % len(self.segments)
+ )
+
+ def load_segment(self, f, is_irom_segment=False):
+ """Load the next segment from the image file"""
+ file_offs = f.tell()
+ (offset, size) = struct.unpack(" 0x40200000 or offset < 0x3FFE0000 or size > 65536:
+ print("WARNING: Suspicious segment 0x%x, length %d" % (offset, size))
+
+ def maybe_patch_segment_data(self, f, segment_data):
+ """
+ If SHA256 digest of the ELF file needs to be inserted into this segment, do so.
+ Returns segment data.
+ """
+ segment_len = len(segment_data)
+ file_pos = f.tell() # file_pos is position in the .bin file
+ if (
+ self.elf_sha256_offset >= file_pos
+ and self.elf_sha256_offset < file_pos + segment_len
+ ):
+ # SHA256 digest needs to be patched into this binary segment,
+ # calculate offset of the digest inside the binary segment.
+ patch_offset = self.elf_sha256_offset - file_pos
+ # Sanity checks
+ if (
+ patch_offset < self.SEG_HEADER_LEN
+ or patch_offset + self.SHA256_DIGEST_LEN > segment_len
+ ):
+ raise FatalError(
+ "Cannot place SHA256 digest on segment boundary"
+ "(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)"
+ % (self.elf_sha256_offset, file_pos, segment_len)
+ )
+ # offset relative to the data part
+ patch_offset -= self.SEG_HEADER_LEN
+ if (
+ segment_data[patch_offset : patch_offset + self.SHA256_DIGEST_LEN]
+ != b"\x00" * self.SHA256_DIGEST_LEN
+ ):
+ raise FatalError(
+ "Contents of segment at SHA256 digest offset 0x%x are not all zero."
+ " Refusing to overwrite." % self.elf_sha256_offset
+ )
+ assert len(self.elf_sha256) == self.SHA256_DIGEST_LEN
+ segment_data = (
+ segment_data[0:patch_offset]
+ + self.elf_sha256
+ + segment_data[patch_offset + self.SHA256_DIGEST_LEN :]
+ )
+ return segment_data
+
+ def save_segment(self, f, segment, checksum=None):
+ """
+ Save the next segment to the image file,
+ return next checksum value if provided
+ """
+ segment_data = self.maybe_patch_segment_data(f, segment.data)
+ f.write(struct.pack(" 0:
+ if len(irom_segments) != 1:
+ raise FatalError(
+ "Found %d segments that could be irom0. Bad ELF file?"
+ % len(irom_segments)
+ )
+ return irom_segments[0]
+ return None
+
+ def get_non_irom_segments(self):
+ irom_segment = self.get_irom_segment()
+ return [s for s in self.segments if s != irom_segment]
+
+ def merge_adjacent_segments(self):
+ if not self.segments:
+ return # nothing to merge
+
+ segments = []
+ # The easiest way to merge the sections is the browse them backward.
+ for i in range(len(self.segments) - 1, 0, -1):
+ # elem is the previous section, the one `next_elem` may need to be
+ # merged in
+ elem = self.segments[i - 1]
+ next_elem = self.segments[i]
+ if all(
+ (
+ elem.get_memory_type(self) == next_elem.get_memory_type(self),
+ elem.include_in_checksum == next_elem.include_in_checksum,
+ next_elem.addr == elem.addr + len(elem.data),
+ )
+ ):
+ # Merge any segment that ends where the next one starts,
+ # without spanning memory types
+ #
+ # (don't 'pad' any gaps here as they may be excluded from the image
+ # due to 'noinit' or other reasons.)
+ elem.data += next_elem.data
+ else:
+ # The section next_elem cannot be merged into the previous one,
+ # which means it needs to be part of the final segments.
+ # As we are browsing the list backward, the elements need to be
+ # inserted at the beginning of the final list.
+ segments.insert(0, next_elem)
+
+ # The first segment will always be here as it cannot be merged into any
+ # "previous" section.
+ segments.insert(0, self.segments[0])
+
+ # note: we could sort segments here as well, but the ordering of segments is
+ # sometimes important for other reasons (like embedded ELF SHA-256),
+ # so we assume that the linker script will have produced any adjacent sections
+ # in linear order in the ELF, anyhow.
+ self.segments = segments
+
+ def set_mmu_page_size(self, size):
+ """
+ If supported, this should be overridden by the chip-specific class.
+ Gets called in elf2image.
+ """
+ print(
+ "WARNING: Changing MMU page size is not supported on {}! "
+ "Defaulting to 64KB.".format(self.ROM_LOADER.CHIP_NAME)
+ )
+
+
+class ESP8266ROMFirmwareImage(BaseFirmwareImage):
+ """'Version 1' firmware image, segments loaded directly by the ROM bootloader."""
+
+ ROM_LOADER = ESP8266ROM
+
+ def __init__(self, load_file=None):
+ super(ESP8266ROMFirmwareImage, self).__init__()
+ self.flash_mode = 0
+ self.flash_size_freq = 0
+ self.version = 1
+
+ if load_file is not None:
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
+
+ for _ in range(segments):
+ self.load_segment(load_file)
+ self.checksum = self.read_checksum(load_file)
+
+ self.verify()
+
+ def default_output_name(self, input_file):
+ """Derive a default output name from the ELF name."""
+ return input_file + "-"
+
+ def save(self, basename):
+ """Save a set of V1 images for flashing. Parameter is a base filename."""
+ # IROM data goes in its own plain binary file
+ irom_segment = self.get_irom_segment()
+ if irom_segment is not None:
+ with open(
+ "%s0x%05x.bin"
+ % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START),
+ "wb",
+ ) as f:
+ f.write(irom_segment.data)
+
+ # everything but IROM goes at 0x00000 in an image file
+ normal_segments = self.get_non_irom_segments()
+ with open("%s0x00000.bin" % basename, "wb") as f:
+ self.write_common_header(f, normal_segments)
+ checksum = ESPLoader.ESP_CHECKSUM_MAGIC
+ for segment in normal_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ self.append_checksum(f, checksum)
+
+
+ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage
+
+
+class ESP8266V2FirmwareImage(BaseFirmwareImage):
+ """'Version 2' firmware image, segments loaded by software bootloader stub
+ (ie Espressif bootloader or rboot)
+ """
+
+ ROM_LOADER = ESP8266ROM
+ # First byte of the "v2" application image
+ IMAGE_V2_MAGIC = 0xEA
+
+ # First 'segment' value in a "v2" application image,
+ # appears to be a constant version value?
+ IMAGE_V2_SEGMENT = 4
+
+ def __init__(self, load_file=None):
+ super(ESP8266V2FirmwareImage, self).__init__()
+ self.version = 2
+ if load_file is not None:
+ segments = self.load_common_header(load_file, self.IMAGE_V2_MAGIC)
+ if segments != self.IMAGE_V2_SEGMENT:
+ # segment count is not really segment count here,
+ # but we expect to see '4'
+ print(
+ 'Warning: V2 header has unexpected "segment" count %d (usually 4)'
+ % segments
+ )
+
+ # irom segment comes before the second header
+ #
+ # the file is saved in the image with a zero load address
+ # in the header, so we need to calculate a load address
+ irom_segment = self.load_segment(load_file, True)
+ # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8
+ irom_segment.addr = 0
+ irom_segment.include_in_checksum = False
+
+ first_flash_mode = self.flash_mode
+ first_flash_size_freq = self.flash_size_freq
+ first_entrypoint = self.entrypoint
+ # load the second header
+
+ segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
+
+ if first_flash_mode != self.flash_mode:
+ print(
+ "WARNING: Flash mode value in first header (0x%02x) disagrees "
+ "with second (0x%02x). Using second value."
+ % (first_flash_mode, self.flash_mode)
+ )
+ if first_flash_size_freq != self.flash_size_freq:
+ print(
+ "WARNING: Flash size/freq value in first header (0x%02x) disagrees "
+ "with second (0x%02x). Using second value."
+ % (first_flash_size_freq, self.flash_size_freq)
+ )
+ if first_entrypoint != self.entrypoint:
+ print(
+ "WARNING: Entrypoint address in first header (0x%08x) disagrees "
+ "with second header (0x%08x). Using second value."
+ % (first_entrypoint, self.entrypoint)
+ )
+
+ # load all the usual segments
+ for _ in range(segments):
+ self.load_segment(load_file)
+ self.checksum = self.read_checksum(load_file)
+
+ self.verify()
+
+ def default_output_name(self, input_file):
+ """Derive a default output name from the ELF name."""
+ irom_segment = self.get_irom_segment()
+ if irom_segment is not None:
+ irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START
+ else:
+ irom_offs = 0
+ return "%s-0x%05x.bin" % (
+ os.path.splitext(input_file)[0],
+ irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1),
+ )
+
+ def save(self, filename):
+ with open(filename, "wb") as f:
+ # Save first header for irom0 segment
+ f.write(
+ struct.pack(
+ b" 0:
+ last_addr = flash_segments[0].addr
+ for segment in flash_segments[1:]:
+ if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
+ raise FatalError(
+ "Segment loaded at 0x%08x lands in same 64KB flash mapping "
+ "as segment loaded at 0x%08x. Can't generate binary. "
+ "Suggest changing linker script or ELF to merge sections."
+ % (segment.addr, last_addr)
+ )
+ last_addr = segment.addr
+
+ def get_alignment_data_needed(segment):
+ # Actual alignment (in data bytes) required for a segment header:
+ # positioned so that after we write the next 8 byte header,
+ # file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN
+ #
+ # (this is because the segment's vaddr may not be IROM_ALIGNed,
+ # more likely is aligned IROM_ALIGN+0x18
+ # to account for the binary file header
+ align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN
+ pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past
+ if pad_len == 0 or pad_len == self.IROM_ALIGN:
+ return 0 # already aligned
+
+ # subtract SEG_HEADER_LEN a second time,
+ # as the padding block has a header as well
+ pad_len -= self.SEG_HEADER_LEN
+ if pad_len < 0:
+ pad_len += self.IROM_ALIGN
+ return pad_len
+
+ # try to fit each flash segment on a 64kB aligned boundary
+ # by padding with parts of the non-flash segments...
+ while len(flash_segments) > 0:
+ segment = flash_segments[0]
+ pad_len = get_alignment_data_needed(segment)
+ if pad_len > 0: # need to pad
+ if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN:
+ pad_segment = ram_segments[0].split_image(pad_len)
+ if len(ram_segments[0].data) == 0:
+ ram_segments.pop(0)
+ else:
+ pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
+ checksum = self.save_segment(f, pad_segment, checksum)
+ total_segments += 1
+ else:
+ # write the flash segment
+ assert (
+ f.tell() + 8
+ ) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN
+ checksum = self.save_flash_segment(f, segment, checksum)
+ flash_segments.pop(0)
+ total_segments += 1
+
+ # flash segments all written, so write any remaining RAM segments
+ for segment in ram_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ total_segments += 1
+
+ if self.secure_pad:
+ # pad the image so that after signing it will end on a a 64KB boundary.
+ # This ensures all mapped flash content will be verified.
+ if not self.append_digest:
+ raise FatalError(
+ "secure_pad only applies if a SHA-256 digest "
+ "is also appended to the image"
+ )
+ align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN
+ # 16 byte aligned checksum
+ # (force the alignment to simplify calculations)
+ checksum_space = 16
+ if self.secure_pad == "1":
+ # after checksum: SHA-256 digest +
+ # (to be added by signing process) version,
+ # signature + 12 trailing bytes due to alignment
+ space_after_checksum = 32 + 4 + 64 + 12
+ elif self.secure_pad == "2": # Secure Boot V2
+ # after checksum: SHA-256 digest +
+ # signature sector,
+ # but we place signature sector after the 64KB boundary
+ space_after_checksum = 32
+ pad_len = (
+ self.IROM_ALIGN - align_past - checksum_space - space_after_checksum
+ ) % self.IROM_ALIGN
+ pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
+
+ checksum = self.save_segment(f, pad_segment, checksum)
+ total_segments += 1
+
+ # done writing segments
+ self.append_checksum(f, checksum)
+ image_length = f.tell()
+
+ if self.secure_pad:
+ assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0
+
+ # kinda hacky: go back to the initial header and write the new segment count
+ # that includes padding segments. This header is not checksummed
+ f.seek(1)
+ f.write(bytes([total_segments]))
+
+ if self.append_digest:
+ # calculate the SHA256 of the whole file and append it
+ f.seek(0)
+ digest = hashlib.sha256()
+ digest.update(f.read(image_length))
+ f.write(digest.digest())
+
+ if self.pad_to_size:
+ image_length = f.tell()
+ if image_length % self.pad_to_size != 0:
+ pad_by = self.pad_to_size - (image_length % self.pad_to_size)
+ f.write(b"\xff" * pad_by)
+
+ with open(filename, "wb") as real_file:
+ real_file.write(f.getvalue())
+
+ def save_flash_segment(self, f, segment, checksum=None):
+ """
+ Save the next segment to the image file, return next checksum value if provided
+ """
+ segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN
+ segment_len_remainder = segment_end_pos % self.IROM_ALIGN
+ if segment_len_remainder < 0x24:
+ # Work around a bug in ESP-IDF 2nd stage bootloader, that it didn't map the
+ # last MMU page, if an IROM/DROM segment was < 0x24 bytes
+ # over the page boundary.
+ segment.data += b"\x00" * (0x24 - segment_len_remainder)
+ return self.save_segment(f, segment, checksum)
+
+ def load_extended_header(self, load_file):
+ def split_byte(n):
+ return (n & 0x0F, (n >> 4) & 0x0F)
+
+ fields = list(
+ struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
+ )
+
+ self.wp_pin = fields[0]
+
+ # SPI pin drive stengths are two per byte
+ self.clk_drv, self.q_drv = split_byte(fields[1])
+ self.d_drv, self.cs_drv = split_byte(fields[2])
+ self.hd_drv, self.wp_drv = split_byte(fields[3])
+
+ self.chip_id = fields[4]
+ if self.chip_id != self.ROM_LOADER.IMAGE_CHIP_ID:
+ print(
+ (
+ "Unexpected chip id in image. Expected %d but value was %d. "
+ "Is this image for a different chip model?"
+ )
+ % (self.ROM_LOADER.IMAGE_CHIP_ID, self.chip_id)
+ )
+
+ self.min_rev = fields[5]
+ self.min_rev_full = fields[6]
+ self.max_rev_full = fields[7]
+
+ # reserved fields in the middle should all be zero
+ if any(f for f in fields[8:-1] if f != 0):
+ print(
+ "Warning: some reserved header fields have non-zero values. "
+ "This image may be from a newer esptool.py?"
+ )
+
+ append_digest = fields[-1] # last byte is append_digest
+ if append_digest in [0, 1]:
+ self.append_digest = append_digest == 1
+ else:
+ raise RuntimeError(
+ "Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
+ append_digest,
+ )
+
+ def save_extended_header(self, save_file):
+ def join_byte(ln, hn):
+ return (ln & 0x0F) + ((hn & 0x0F) << 4)
+
+ append_digest = 1 if self.append_digest else 0
+
+ fields = [
+ self.wp_pin,
+ join_byte(self.clk_drv, self.q_drv),
+ join_byte(self.d_drv, self.cs_drv),
+ join_byte(self.hd_drv, self.wp_drv),
+ self.ROM_LOADER.IMAGE_CHIP_ID,
+ self.min_rev,
+ self.min_rev_full,
+ self.max_rev_full,
+ ]
+ fields += [0] * 4 # padding
+ fields += [append_digest]
+
+ packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields)
+ save_file.write(packed)
+
+
+class ESP8266V3FirmwareImage(ESP32FirmwareImage):
+ """ESP8266 V3 firmware image is very similar to ESP32 image"""
+
+ EXTENDED_HEADER_STRUCT_FMT = "B" * 16
+
+ def is_flash_addr(self, addr):
+ return addr > ESP8266ROM.IROM_MAP_START
+
+ def save(self, filename):
+ total_segments = 0
+ with io.BytesIO() as f: # write file to memory first
+ self.write_common_header(f, self.segments)
+
+ checksum = ESPLoader.ESP_CHECKSUM_MAGIC
+
+ # split segments into flash-mapped vs ram-loaded,
+ # and take copies so we can mutate them
+ flash_segments = [
+ copy.deepcopy(s)
+ for s in sorted(self.segments, key=lambda s: s.addr)
+ if self.is_flash_addr(s.addr) and len(s.data)
+ ]
+ ram_segments = [
+ copy.deepcopy(s)
+ for s in sorted(self.segments, key=lambda s: s.addr)
+ if not self.is_flash_addr(s.addr) and len(s.data)
+ ]
+
+ # check for multiple ELF sections that are mapped in the same
+ # flash mapping region. This is usually a sign of a broken linker script,
+ # but if you have a legitimate use case then let us know
+ if len(flash_segments) > 0:
+ last_addr = flash_segments[0].addr
+ for segment in flash_segments[1:]:
+ if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
+ raise FatalError(
+ "Segment loaded at 0x%08x lands in same 64KB flash mapping "
+ "as segment loaded at 0x%08x. Can't generate binary. "
+ "Suggest changing linker script or ELF to merge sections."
+ % (segment.addr, last_addr)
+ )
+ last_addr = segment.addr
+
+ # try to fit each flash segment on a 64kB aligned boundary
+ # by padding with parts of the non-flash segments...
+ while len(flash_segments) > 0:
+ segment = flash_segments[0]
+ # remove 8 bytes empty data for insert segment header
+ if segment.name == ".flash.rodata":
+ segment.data = segment.data[8:]
+ # write the flash segment
+ checksum = self.save_segment(f, segment, checksum)
+ flash_segments.pop(0)
+ total_segments += 1
+
+ # flash segments all written, so write any remaining RAM segments
+ for segment in ram_segments:
+ checksum = self.save_segment(f, segment, checksum)
+ total_segments += 1
+
+ # done writing segments
+ self.append_checksum(f, checksum)
+ image_length = f.tell()
+
+ # kinda hacky: go back to the initial header and write the new segment count
+ # that includes padding segments. This header is not checksummed
+ f.seek(1)
+ f.write(bytes([total_segments]))
+
+ if self.append_digest:
+ # calculate the SHA256 of the whole file and append it
+ f.seek(0)
+ digest = hashlib.sha256()
+ digest.update(f.read(image_length))
+ f.write(digest.digest())
+
+ with open(filename, "wb") as real_file:
+ real_file.write(f.getvalue())
+
+ def load_extended_header(self, load_file):
+ def split_byte(n):
+ return (n & 0x0F, (n >> 4) & 0x0F)
+
+ fields = list(
+ struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
+ )
+
+ self.wp_pin = fields[0]
+
+ # SPI pin drive stengths are two per byte
+ self.clk_drv, self.q_drv = split_byte(fields[1])
+ self.d_drv, self.cs_drv = split_byte(fields[2])
+ self.hd_drv, self.wp_drv = split_byte(fields[3])
+
+ if fields[15] in [0, 1]:
+ self.append_digest = fields[15] == 1
+ else:
+ raise RuntimeError(
+ "Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
+ fields[15],
+ )
+
+ # remaining fields in the middle should all be zero
+ if any(f for f in fields[4:15] if f != 0):
+ print(
+ "Warning: some reserved header fields have non-zero values. "
+ "This image may be from a newer esptool.py?"
+ )
+
+
+ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage
+
+
+class ESP32S2FirmwareImage(ESP32FirmwareImage):
+ """ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S2ROM
+
+
+ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage
+
+
+class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage):
+ """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S3BETA2ROM
+
+
+ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage
+
+
+class ESP32S3FirmwareImage(ESP32FirmwareImage):
+ """ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32S3ROM
+
+
+ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage
+
+
+class ESP32C3FirmwareImage(ESP32FirmwareImage):
+ """ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C3ROM
+
+
+ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage
+
+
+class ESP32C6BETAFirmwareImage(ESP32FirmwareImage):
+ """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C6BETAROM
+
+
+ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage
+
+
+class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2BETA1ROM
+
+
+ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage
+
+
+class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2BETA2ROM
+
+
+ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage
+
+
+class ESP32C2FirmwareImage(ESP32FirmwareImage):
+ """ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C2ROM
+
+ def set_mmu_page_size(self, size):
+ if size not in [16384, 32768, 65536]:
+ raise FatalError(
+ "{} bytes is not a valid ESP32-C2 page size, "
+ "select from 64KB, 32KB, 16KB.".format(size)
+ )
+ self.IROM_ALIGN = size
+
+
+ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage
+
+
+class ESP32C6FirmwareImage(ESP32FirmwareImage):
+ """ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32C6ROM
+
+ def set_mmu_page_size(self, size):
+ if size not in [8192, 16384, 32768, 65536]:
+ raise FatalError(
+ "{} bytes is not a valid ESP32-C6 page size, "
+ "select from 64KB, 32KB, 16KB, 8KB.".format(size)
+ )
+ self.IROM_ALIGN = size
+
+
+ESP32C6ROM.BOOTLOADER_IMAGE = ESP32C6FirmwareImage
+
+
+class ESP32H2FirmwareImage(ESP32C6FirmwareImage):
+ """ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
+
+ ROM_LOADER = ESP32H2ROM
+
+
+ESP32H2ROM.BOOTLOADER_IMAGE = ESP32H2FirmwareImage
+
+
+class ELFFile(object):
+ SEC_TYPE_PROGBITS = 0x01
+ SEC_TYPE_STRTAB = 0x03
+ SEC_TYPE_INITARRAY = 0x0E
+ SEC_TYPE_FINIARRAY = 0x0F
+
+ PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY)
+
+ LEN_SEC_HEADER = 0x28
+
+ SEG_TYPE_LOAD = 0x01
+ LEN_SEG_HEADER = 0x20
+
+ def __init__(self, name):
+ # Load sections from the ELF file
+ self.name = name
+ with open(self.name, "rb") as f:
+ self._read_elf_file(f)
+
+ def get_section(self, section_name):
+ for s in self.sections:
+ if s.name == section_name:
+ return s
+ raise ValueError("No section %s in ELF file" % section_name)
+
+ def _read_elf_file(self, f):
+ # read the ELF file header
+ LEN_FILE_HEADER = 0x34
+ try:
+ (
+ ident,
+ _type,
+ machine,
+ _version,
+ self.entrypoint,
+ _phoff,
+ shoff,
+ _flags,
+ _ehsize,
+ _phentsize,
+ _phnum,
+ shentsize,
+ shnum,
+ shstrndx,
+ ) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER))
+ except struct.error as e:
+ raise FatalError(
+ "Failed to read a valid ELF header from %s: %s" % (self.name, e)
+ )
+
+ if byte(ident, 0) != 0x7F or ident[1:4] != b"ELF":
+ raise FatalError("%s has invalid ELF magic header" % self.name)
+ if machine not in [0x5E, 0xF3]:
+ raise FatalError(
+ "%s does not appear to be an Xtensa or an RISCV ELF file. "
+ "e_machine=%04x" % (self.name, machine)
+ )
+ if shentsize != self.LEN_SEC_HEADER:
+ raise FatalError(
+ "%s has unexpected section header entry size 0x%x (not 0x%x)"
+ % (self.name, shentsize, self.LEN_SEC_HEADER)
+ )
+ if shnum == 0:
+ raise FatalError("%s has 0 section headers" % (self.name))
+ self._read_sections(f, shoff, shnum, shstrndx)
+ self._read_segments(f, _phoff, _phnum, shstrndx)
+
+ def _read_sections(self, f, section_header_offs, section_header_count, shstrndx):
+ f.seek(section_header_offs)
+ len_bytes = section_header_count * self.LEN_SEC_HEADER
+ section_header = f.read(len_bytes)
+ if len(section_header) == 0:
+ raise FatalError(
+ "No section header found at offset %04x in ELF file."
+ % section_header_offs
+ )
+ if len(section_header) != (len_bytes):
+ raise FatalError(
+ "Only read 0x%x bytes from section header (expected 0x%x.) "
+ "Truncated ELF file?" % (len(section_header), len_bytes)
+ )
+
+ # walk through the section header and extract all sections
+ section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER)
+
+ def read_section_header(offs):
+ name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(
+ " 0
+ ]
+ self.sections = prog_sections
+
+ def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx):
+ f.seek(segment_header_offs)
+ len_bytes = segment_header_count * self.LEN_SEG_HEADER
+ segment_header = f.read(len_bytes)
+ if len(segment_header) == 0:
+ raise FatalError(
+ "No segment header found at offset %04x in ELF file."
+ % segment_header_offs
+ )
+ if len(segment_header) != (len_bytes):
+ raise FatalError(
+ "Only read 0x%x bytes from segment header (expected 0x%x.) "
+ "Truncated ELF file?" % (len(segment_header), len_bytes)
+ )
+
+ # walk through the segment header and extract all segments
+ segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER)
+
+ def read_segment_header(offs):
+ (
+ seg_type,
+ seg_offs,
+ _vaddr,
+ lma,
+ size,
+ _memsize,
+ _flags,
+ _align,
+ ) = struct.unpack_from(" 0
+ ]
+ self.segments = prog_segments
+
+ def sha256(self):
+ # return SHA256 hash of the input ELF file
+ sha256 = hashlib.sha256()
+ with open(self.name, "rb") as f:
+ sha256.update(f.read())
+ return sha256.digest()
diff --git a/installer/bin/esptool/esptool/cmds.py b/installer/bin/esptool/esptool/cmds.py
new file mode 100644
index 0000000..a299058
--- /dev/null
+++ b/installer/bin/esptool/esptool/cmds.py
@@ -0,0 +1,1198 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import hashlib
+import io
+import os
+import struct
+import sys
+import time
+import zlib
+
+from .bin_image import ELFFile, ImageSegment, LoadFirmwareImage
+from .bin_image import (
+ ESP8266ROMFirmwareImage,
+ ESP8266V2FirmwareImage,
+ ESP8266V3FirmwareImage,
+)
+from .loader import (
+ DEFAULT_CONNECT_ATTEMPTS,
+ DEFAULT_TIMEOUT,
+ ERASE_WRITE_TIMEOUT_PER_MB,
+ ESPLoader,
+ timeout_per_mb,
+)
+from .targets import CHIP_DEFS, CHIP_LIST, ROM_LIST
+from .util import (
+ FatalError,
+ NotImplementedInROMError,
+ NotSupportedError,
+ UnsupportedCommandError,
+)
+from .util import (
+ div_roundup,
+ flash_size_bytes,
+ hexify,
+ pad_to,
+ print_overwrite,
+)
+
+DETECTED_FLASH_SIZES = {
+ 0x12: "256KB",
+ 0x13: "512KB",
+ 0x14: "1MB",
+ 0x15: "2MB",
+ 0x16: "4MB",
+ 0x17: "8MB",
+ 0x18: "16MB",
+ 0x19: "32MB",
+ 0x1A: "64MB",
+ 0x1B: "128MB",
+ 0x1C: "256MB",
+ 0x20: "64MB",
+ 0x21: "128MB",
+ 0x22: "256MB",
+ 0x32: "256KB",
+ 0x33: "512KB",
+ 0x34: "1MB",
+ 0x35: "2MB",
+ 0x36: "4MB",
+ 0x37: "8MB",
+ 0x38: "16MB",
+ 0x39: "32MB",
+ 0x3A: "64MB",
+}
+
+FLASH_MODES = {"qio": 0, "qout": 1, "dio": 2, "dout": 3}
+
+
+def detect_chip(
+ port=ESPLoader.DEFAULT_PORT,
+ baud=ESPLoader.ESP_ROM_BAUD,
+ connect_mode="default_reset",
+ trace_enabled=False,
+ connect_attempts=DEFAULT_CONNECT_ATTEMPTS,
+):
+ """Use serial access to detect the chip type.
+
+ First, get_security_info command is sent to detect the ID of the chip
+ (supported only by ESP32-C3 and later, works even in the Secure Download Mode).
+ If this fails, we reconnect and fall-back to reading the magic number.
+ It's mapped at a specific ROM address and has a different value on each chip model.
+ This way we use one memory read and compare it to the magic number for each chip.
+
+ This routine automatically performs ESPLoader.connect() (passing
+ connect_mode parameter) as part of querying the chip.
+ """
+ inst = None
+ detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled)
+ if detect_port.serial_port.startswith("rfc2217:"):
+ detect_port.USES_RFC2217 = True
+ detect_port.connect(connect_mode, connect_attempts, detecting=True)
+ try:
+ print("Detecting chip type...", end="")
+ chip_id = detect_port.get_chip_id()
+ for cls in [
+ n for n in ROM_LIST if n.CHIP_NAME not in ("ESP8266", "ESP32", "ESP32S2")
+ ]:
+ # cmd not supported on ESP8266 and ESP32 + ESP32-S2 doesn't return chip_id
+ if chip_id == cls.IMAGE_CHIP_ID:
+ inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
+ try:
+ inst.read_reg(
+ ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR
+ ) # Dummy read to check Secure Download mode
+ except UnsupportedCommandError:
+ inst.secure_download_mode = True
+ inst._post_connect()
+ break
+ else:
+ err_msg = f"Unexpected chip ID value {chip_id}."
+ except (UnsupportedCommandError, struct.error, FatalError) as e:
+ # UnsupportedCommmanddError: ESP8266/ESP32 ROM
+ # struct.error: ESP32-S2
+ # FatalError: ESP8266/ESP32 STUB
+ print(" Unsupported detection protocol, switching and trying again...")
+ try:
+ # ESP32/ESP8266 are reset after an unsupported command, need to reconnect
+ # (not needed on ESP32-S2)
+ if not isinstance(e, struct.error):
+ detect_port.connect(
+ connect_mode, connect_attempts, detecting=True, warnings=False
+ )
+ print("Detecting chip type...", end="")
+ sys.stdout.flush()
+ chip_magic_value = detect_port.read_reg(
+ ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR
+ )
+
+ for cls in ROM_LIST:
+ if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
+ inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
+ inst._post_connect()
+ inst.check_chip_id()
+ break
+ else:
+ err_msg = f"Unexpected chip magic value {chip_magic_value:#010x}."
+ except UnsupportedCommandError:
+ raise FatalError(
+ "Unsupported Command Error received. "
+ "Probably this means Secure Download Mode is enabled, "
+ "autodetection will not work. Need to manually specify the chip."
+ )
+ finally:
+ if inst is not None:
+ print(" %s" % inst.CHIP_NAME, end="")
+ if detect_port.sync_stub_detected:
+ inst = inst.STUB_CLASS(inst)
+ inst.sync_stub_detected = True
+ print("") # end line
+ return inst
+ raise FatalError(
+ f"{err_msg} Failed to autodetect chip type."
+ "\nProbably it is unsupported by this version of esptool."
+ )
+
+
+# "Operation" commands, executable at command line. One function each
+#
+# Each function takes either two args (, ) or a single
+# argument.
+
+
+def load_ram(esp, args):
+ image = LoadFirmwareImage(esp.CHIP_NAME, args.filename)
+
+ print("RAM boot...")
+ for seg in image.segments:
+ size = len(seg.data)
+ print("Downloading %d bytes at %08x..." % (size, seg.addr), end=" ")
+ sys.stdout.flush()
+ esp.mem_begin(
+ size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr
+ )
+
+ seq = 0
+ while len(seg.data) > 0:
+ esp.mem_block(seg.data[0 : esp.ESP_RAM_BLOCK], seq)
+ seg.data = seg.data[esp.ESP_RAM_BLOCK :]
+ seq += 1
+ print("done!")
+
+ print("All segments done, executing at %08x" % image.entrypoint)
+ esp.mem_finish(image.entrypoint)
+
+
+def read_mem(esp, args):
+ print("0x%08x = 0x%08x" % (args.address, esp.read_reg(args.address)))
+
+
+def write_mem(esp, args):
+ esp.write_reg(args.address, args.value, args.mask, 0)
+ print("Wrote %08x, mask %08x to %08x" % (args.value, args.mask, args.address))
+
+
+def dump_mem(esp, args):
+ with open(args.filename, "wb") as f:
+ for i in range(args.size // 4):
+ d = esp.read_reg(args.address + (i * 4))
+ f.write(struct.pack(b"> 16
+ args.flash_size = DETECTED_FLASH_SIZES.get(size_id)
+ if args.flash_size is None:
+ print(
+ "Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x),"
+ " defaulting to 4MB" % (flash_id, size_id)
+ )
+ args.flash_size = "4MB"
+ else:
+ print("Auto-detected Flash size:", args.flash_size)
+
+
+def _update_image_flash_params(esp, address, args, image):
+ """
+ Modify the flash mode & size bytes if this looks like an executable bootloader image
+ """
+ if len(image) < 8:
+ return image # not long enough to be a bootloader image
+
+ # unpack the (potential) image header
+ magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4])
+ if address != esp.BOOTLOADER_FLASH_OFFSET:
+ return image # not flashing bootloader offset, so don't modify this
+
+ if (args.flash_mode, args.flash_freq, args.flash_size) == ("keep",) * 3:
+ return image # all settings are 'keep', not modifying anything
+
+ # easy check if this is an image: does it start with a magic byte?
+ if magic != esp.ESP_IMAGE_MAGIC:
+ print(
+ "Warning: Image file at 0x%x doesn't look like an image file, "
+ "so not changing any flash settings." % address
+ )
+ return image
+
+ # make sure this really is an image, and not just data that
+ # starts with esp.ESP_IMAGE_MAGIC (mostly a problem for encrypted
+ # images that happen to start with a magic byte
+ try:
+ test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image))
+ test_image.verify()
+ except Exception:
+ print(
+ "Warning: Image file at 0x%x is not a valid %s image, "
+ "so not changing any flash settings." % (address, esp.CHIP_NAME)
+ )
+ return image
+
+ # After the 8-byte header comes the extended header for chips others than ESP8266.
+ # The 15th byte of the extended header indicates if the image is protected by
+ # a SHA256 checksum. In that case we should not modify the header because
+ # the checksum check would fail.
+ sha_implies_keep = args.chip != "esp8266" and image[8 + 15] == 1
+
+ def print_keep_warning(arg_to_keep, arg_used):
+ print(
+ "Warning: Image file at {addr} is protected with a hash checksum, "
+ "so not changing the flash {arg} setting. "
+ "Use the --flash_{arg}=keep option instead of --flash_{arg}={arg_orig} "
+ "in order to remove this warning, or use the --dont-append-digest option "
+ "for the elf2image command in order to generate an image file "
+ "without a hash checksum".format(
+ addr=hex(address), arg=arg_to_keep, arg_orig=arg_used
+ )
+ )
+
+ if args.flash_mode != "keep":
+ new_flash_mode = FLASH_MODES[args.flash_mode]
+ if flash_mode != new_flash_mode and sha_implies_keep:
+ print_keep_warning("mode", args.flash_mode)
+ else:
+ flash_mode = new_flash_mode
+
+ flash_freq = flash_size_freq & 0x0F
+ if args.flash_freq != "keep":
+ new_flash_freq = esp.parse_flash_freq_arg(args.flash_freq)
+ if flash_freq != new_flash_freq and sha_implies_keep:
+ print_keep_warning("frequency", args.flash_freq)
+ else:
+ flash_freq = new_flash_freq
+
+ flash_size = flash_size_freq & 0xF0
+ if args.flash_size != "keep":
+ new_flash_size = esp.parse_flash_size_arg(args.flash_size)
+ if flash_size != new_flash_size and sha_implies_keep:
+ print_keep_warning("size", args.flash_size)
+ else:
+ flash_size = new_flash_size
+
+ flash_params = struct.pack(b"BB", flash_mode, flash_size + flash_freq)
+ if flash_params != image[2:4]:
+ print("Flash params set to 0x%04x" % struct.unpack(">H", flash_params))
+ image = image[0:2] + flash_params + image[4:]
+ return image
+
+
+def write_flash(esp, args):
+ # set args.compress based on default behaviour:
+ # -> if either --compress or --no-compress is set, honour that
+ # -> otherwise, set --compress unless --no-stub is set
+ if args.compress is None and not args.no_compress:
+ args.compress = not args.no_stub
+
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ # Check if secure boot is active
+ if esp.get_secure_boot_enabled():
+ for address, _ in args.addr_filename:
+ if address < 0x8000:
+ raise FatalError(
+ "Secure Boot detected, writing to flash regions < 0x8000 "
+ "is disabled to protect the bootloader. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ # Check if chip_id and min_rev in image are valid for the target in use
+ for _, argfile in args.addr_filename:
+ try:
+ image = LoadFirmwareImage(esp.CHIP_NAME, argfile)
+ except (FatalError, struct.error, RuntimeError):
+ continue
+ finally:
+ argfile.seek(0) # LoadFirmwareImage changes the file handle position
+ if image.chip_id != esp.IMAGE_CHIP_ID:
+ raise FatalError(
+ f"{argfile.name} is not an {esp.CHIP_NAME} image. "
+ "Use --force to flash anyway."
+ )
+
+ # this logic below decides which min_rev to use, min_rev or min/max_rev_full
+ if image.max_rev_full == 0: # image does not have max/min_rev_full fields
+ use_rev_full_fields = False
+ elif image.max_rev_full == 65535: # image has default value of max_rev_full
+ use_rev_full_fields = True
+ if (
+ image.min_rev_full == 0 and image.min_rev != 0
+ ): # min_rev_full is not set, min_rev is used
+ use_rev_full_fields = False
+ else: # max_rev_full set to a version
+ use_rev_full_fields = True
+
+ if use_rev_full_fields:
+ rev = esp.get_chip_revision()
+ if rev < image.min_rev_full or rev > image.max_rev_full:
+ error_str = f"{argfile.name} requires chip revision in range "
+ error_str += (
+ f"[v{image.min_rev_full // 100}.{image.min_rev_full % 100} - "
+ )
+ if image.max_rev_full == 65535:
+ error_str += "max rev not set] "
+ else:
+ error_str += (
+ f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}] "
+ )
+ error_str += f"(this chip is revision v{rev // 100}.{rev % 100})"
+ raise FatalError(f"{error_str}. Use --force to flash anyway.")
+ else:
+ # In IDF, image.min_rev is set based on Kconfig option.
+ # For C3 chip, image.min_rev is the Minor revision
+ # while for the rest chips it is the Major revision.
+ if esp.CHIP_NAME == "ESP32-C3":
+ rev = esp.get_minor_chip_version()
+ else:
+ rev = esp.get_major_chip_version()
+ if rev < image.min_rev:
+ raise FatalError(
+ f"{argfile.name} requires chip revision "
+ f"{image.min_rev} or higher (this chip is revision {rev}). "
+ "Use --force to flash anyway."
+ )
+
+ # In case we have encrypted files to write,
+ # we first do few sanity checks before actual flash
+ if args.encrypt or args.encrypt_files is not None:
+ do_write = True
+
+ if not esp.secure_download_mode:
+ if esp.get_encrypted_download_disabled():
+ raise FatalError(
+ "This chip has encrypt functionality "
+ "in UART download mode disabled. "
+ "This is the Flash Encryption configuration for Production mode "
+ "instead of Development mode."
+ )
+
+ crypt_cfg_efuse = esp.get_flash_crypt_config()
+
+ if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF:
+ print("Unexpected FLASH_CRYPT_CONFIG value: 0x%x" % (crypt_cfg_efuse))
+ do_write = False
+
+ enc_key_valid = esp.is_flash_encryption_key_valid()
+
+ if not enc_key_valid:
+ print("Flash encryption key is not programmed")
+ do_write = False
+
+ # Determine which files list contain the ones to encrypt
+ files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files
+
+ for address, argfile in files_to_encrypt:
+ if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN:
+ print(
+ "File %s address 0x%x is not %d byte aligned, can't flash encrypted"
+ % (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN)
+ )
+ do_write = False
+
+ if not do_write and not args.ignore_flash_encryption_efuse_setting:
+ raise FatalError(
+ "Can't perform encrypted flash write, "
+ "consult Flash Encryption documentation for more information"
+ )
+ else:
+ if not args.force and esp.CHIP_NAME != "ESP8266":
+ # ESP32 does not support `get_security_info()` and `secure_download_mode`
+ if (
+ esp.CHIP_NAME != "ESP32"
+ and esp.secure_download_mode
+ and bin(esp.get_security_info()["flash_crypt_cnt"]).count("1") & 1 != 0
+ ):
+ raise FatalError(
+ "WARNING: Detected flash encryption and "
+ "secure download mode enabled.\n"
+ "Flashing plaintext binary may brick your device! "
+ "Use --force to override the warning."
+ )
+
+ if (
+ not esp.secure_download_mode
+ and esp.get_encrypted_download_disabled()
+ and esp.get_flash_encryption_enabled()
+ ):
+ raise FatalError(
+ "WARNING: Detected flash encryption enabled and "
+ "download manual encrypt disabled.\n"
+ "Flashing plaintext binary may brick your device! "
+ "Use --force to override the warning."
+ )
+
+ # verify file sizes fit in flash
+ if args.flash_size != "keep": # TODO: check this even with 'keep'
+ flash_end = flash_size_bytes(args.flash_size)
+ for address, argfile in args.addr_filename:
+ argfile.seek(0, os.SEEK_END)
+ if address + argfile.tell() > flash_end:
+ raise FatalError(
+ "File %s (length %d) at offset %d "
+ "will not fit in %d bytes of flash. "
+ "Use --flash_size argument, or change flashing address."
+ % (argfile.name, argfile.tell(), address, flash_end)
+ )
+ argfile.seek(0)
+
+ if args.erase_all:
+ erase_flash(esp, args)
+ else:
+ for address, argfile in args.addr_filename:
+ argfile.seek(0, os.SEEK_END)
+ write_end = address + argfile.tell()
+ argfile.seek(0)
+ bytes_over = address % esp.FLASH_SECTOR_SIZE
+ if bytes_over != 0:
+ print(
+ "WARNING: Flash address {:#010x} is not aligned "
+ "to a {:#x} byte flash sector. "
+ "{:#x} bytes before this address will be erased.".format(
+ address, esp.FLASH_SECTOR_SIZE, bytes_over
+ )
+ )
+ # Print the address range of to-be-erased flash memory region
+ print(
+ "Flash will be erased from {:#010x} to {:#010x}...".format(
+ address - bytes_over,
+ div_roundup(write_end, esp.FLASH_SECTOR_SIZE)
+ * esp.FLASH_SECTOR_SIZE
+ - 1,
+ )
+ )
+
+ """ Create a list describing all the files we have to flash.
+ Each entry holds an "encrypt" flag marking whether the file needs encryption or not.
+ This list needs to be sorted.
+
+ First, append to each entry of our addr_filename list the flag args.encrypt
+ E.g., if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")],
+ all_files will be [
+ (0x1000, "partition.bin", args.encrypt),
+ (0x8000, "bootloader", args.encrypt)
+ ],
+ where, of course, args.encrypt is either True or False
+ """
+ all_files = [
+ (offs, filename, args.encrypt) for (offs, filename) in args.addr_filename
+ ]
+
+ """
+ Now do the same with encrypt_files list, if defined.
+ In this case, the flag is True
+ """
+ if args.encrypt_files is not None:
+ encrypted_files_flag = [
+ (offs, filename, True) for (offs, filename) in args.encrypt_files
+ ]
+
+ # Concatenate both lists and sort them.
+ # As both list are already sorted, we could simply do a merge instead,
+ # but for the sake of simplicity and because the lists are very small,
+ # let's use sorted.
+ all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0])
+
+ for address, argfile, encrypted in all_files:
+ compress = args.compress
+
+ # Check whether we can compress the current file before flashing
+ if compress and encrypted:
+ print("\nWARNING: - compress and encrypt options are mutually exclusive ")
+ print("Will flash %s uncompressed" % argfile.name)
+ compress = False
+
+ if args.no_stub:
+ print("Erasing flash...")
+ image = pad_to(
+ argfile.read(), esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4
+ )
+ if len(image) == 0:
+ print("WARNING: File %s is empty" % argfile.name)
+ continue
+ image = _update_image_flash_params(esp, address, args, image)
+ calcmd5 = hashlib.md5(image).hexdigest()
+ uncsize = len(image)
+ if compress:
+ uncimage = image
+ image = zlib.compress(uncimage, 9)
+ # Decompress the compressed binary a block at a time,
+ # to dynamically calculate the timeout based on the real write size
+ decompress = zlib.decompressobj()
+ blocks = esp.flash_defl_begin(uncsize, len(image), address)
+ else:
+ blocks = esp.flash_begin(uncsize, address, begin_rom_encrypted=encrypted)
+ argfile.seek(0) # in case we need it again
+ seq = 0
+ bytes_sent = 0 # bytes sent on wire
+ bytes_written = 0 # bytes written to flash
+ t = time.time()
+
+ timeout = DEFAULT_TIMEOUT
+
+ while len(image) > 0:
+ print_overwrite(
+ "Writing at 0x%08x... (%d %%)"
+ % (address + bytes_written, 100 * (seq + 1) // blocks)
+ )
+ sys.stdout.flush()
+ block = image[0 : esp.FLASH_WRITE_SIZE]
+ if compress:
+ # feeding each compressed block into the decompressor lets us
+ # see block-by-block how much will be written
+ block_uncompressed = len(decompress.decompress(block))
+ bytes_written += block_uncompressed
+ block_timeout = max(
+ DEFAULT_TIMEOUT,
+ timeout_per_mb(ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed),
+ )
+ if not esp.IS_STUB:
+ timeout = (
+ block_timeout # ROM code writes block to flash before ACKing
+ )
+ esp.flash_defl_block(block, seq, timeout=timeout)
+ if esp.IS_STUB:
+ # Stub ACKs when block is received,
+ # then writes to flash while receiving the block after it
+ timeout = block_timeout
+ else:
+ # Pad the last block
+ block = block + b"\xff" * (esp.FLASH_WRITE_SIZE - len(block))
+ if encrypted:
+ esp.flash_encrypt_block(block, seq)
+ else:
+ esp.flash_block(block, seq)
+ bytes_written += len(block)
+ bytes_sent += len(block)
+ image = image[esp.FLASH_WRITE_SIZE :]
+ seq += 1
+
+ if esp.IS_STUB:
+ # Stub only writes each block to flash after 'ack'ing the receive,
+ # so do a final dummy operation which will not be 'ack'ed
+ # until the last block has actually been written out to flash
+ esp.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR, timeout=timeout)
+
+ t = time.time() - t
+ speed_msg = ""
+ if compress:
+ if t > 0.0:
+ speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000)
+ print_overwrite(
+ "Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s..."
+ % (uncsize, bytes_sent, address, t, speed_msg),
+ last_line=True,
+ )
+ else:
+ if t > 0.0:
+ speed_msg = " (%.1f kbit/s)" % (bytes_written / t * 8 / 1000)
+ print_overwrite(
+ "Wrote %d bytes at 0x%08x in %.1f seconds%s..."
+ % (bytes_written, address, t, speed_msg),
+ last_line=True,
+ )
+
+ if not encrypted and not esp.secure_download_mode:
+ try:
+ res = esp.flash_md5sum(address, uncsize)
+ if res != calcmd5:
+ print("File md5: %s" % calcmd5)
+ print("Flash md5: %s" % res)
+ print(
+ "MD5 of 0xFF is %s"
+ % (hashlib.md5(b"\xFF" * uncsize).hexdigest())
+ )
+ raise FatalError("MD5 of file does not match data in flash!")
+ else:
+ print("Hash of data verified.")
+ except NotImplementedInROMError:
+ pass
+
+ print("\nLeaving...")
+
+ if esp.IS_STUB:
+ # skip sending flash_finish to ROM loader here,
+ # as it causes the loader to exit and run user code
+ esp.flash_begin(0, 0)
+
+ # Get the "encrypted" flag for the last file flashed
+ # Note: all_files list contains triplets like:
+ # (address: Integer, filename: String, encrypted: Boolean)
+ last_file_encrypted = all_files[-1][2]
+
+ # Check whether the last file flashed was compressed or not
+ if args.compress and not last_file_encrypted:
+ esp.flash_defl_finish(False)
+ else:
+ esp.flash_finish(False)
+
+ if args.verify:
+ print("Verifying just-written flash...")
+ print(
+ "(This option is deprecated, "
+ "flash contents are now always read back after flashing.)"
+ )
+ # If some encrypted files have been flashed,
+ # print a warning saying that we won't check them
+ if args.encrypt or args.encrypt_files is not None:
+ print("WARNING: - cannot verify encrypted files, they will be ignored")
+ # Call verify_flash function only if there is at least
+ # one non-encrypted file flashed
+ if not args.encrypt:
+ verify_flash(esp, args)
+
+
+def image_info(args):
+ def v2():
+ def get_key_from_value(dict, val):
+ """Get key from value in dictionary"""
+ for key, value in dict.items():
+ if value == val:
+ return key
+ return None
+
+ print()
+ title = "{} image header".format(args.chip.upper())
+ print(title)
+ print("=" * len(title))
+ print("Image version: {}".format(image.version))
+ print(
+ "Entry point: {:#8x}".format(image.entrypoint)
+ if image.entrypoint != 0
+ else "Entry point not set"
+ )
+
+ print("Segments: {}".format(len(image.segments)))
+
+ # Flash size
+ flash_s_bits = image.flash_size_freq & 0xF0 # high four bits
+ flash_s = get_key_from_value(image.ROM_LOADER.FLASH_SIZES, flash_s_bits)
+ print(
+ "Flash size: {}".format(flash_s)
+ if flash_s is not None
+ else "WARNING: Invalid flash size ({:#02x})".format(flash_s_bits)
+ )
+
+ # Flash frequency
+ flash_fr_bits = image.flash_size_freq & 0x0F # low four bits
+ flash_fr = get_key_from_value(image.ROM_LOADER.FLASH_FREQUENCY, flash_fr_bits)
+ print(
+ "Flash freq: {}".format(flash_fr)
+ if flash_fr is not None
+ else "WARNING: Invalid flash frequency ({:#02x})".format(flash_fr_bits)
+ )
+
+ # Flash mode
+ flash_mode = get_key_from_value(FLASH_MODES, image.flash_mode)
+ print(
+ "Flash mode: {}".format(flash_mode.upper())
+ if flash_mode is not None
+ else "WARNING: Invalid flash mode ({})".format(image.flash_mode)
+ )
+
+ # Extended header (ESP32 and later only)
+ if args.chip != "esp8266":
+ print()
+ title = "{} extended image header".format(args.chip.upper())
+ print(title)
+ print("=" * len(title))
+ print("WP pin: {:#02x}".format(image.wp_pin))
+ print(
+ "Flash pins drive settings: "
+ "clk_drv: {:#02x}, q_drv: {:#02x}, d_drv: {:#02x}, "
+ "cs0_drv: {:#02x}, hd_drv: {:#02x}, wp_drv: {:#02x}".format(
+ image.clk_drv,
+ image.q_drv,
+ image.d_drv,
+ image.cs_drv,
+ image.hd_drv,
+ image.wp_drv,
+ )
+ )
+ print("Chip ID: {}".format(image.chip_id))
+ print(
+ "Minimal chip revision: "
+ f"v{image.min_rev_full // 100}.{image.min_rev_full % 100}, "
+ f"(legacy min_rev = {image.min_rev})"
+ )
+ print(
+ "Maximal chip revision: "
+ f"v{image.max_rev_full // 100}.{image.max_rev_full % 100}"
+ )
+ print()
+
+ # Segments overview
+ title = "Segments information"
+ print(title)
+ print("=" * len(title))
+ headers_str = "{:>7} {:>7} {:>10} {:>10} {:10}"
+ print(
+ headers_str.format(
+ "Segment", "Length", "Load addr", "File offs", "Memory types"
+ )
+ )
+ print(
+ "{} {} {} {} {}".format("-" * 7, "-" * 7, "-" * 10, "-" * 10, "-" * 12)
+ )
+ format_str = "{:7} {:#07x} {:#010x} {:#010x} {}"
+ app_desc = None
+ for idx, seg in enumerate(image.segments, start=1):
+ segs = seg.get_memory_type(image)
+ seg_name = ", ".join(segs)
+ if "DROM" in segs: # The DROM segment starts with the esp_app_desc_t struct
+ app_desc = seg.data[:256]
+ print(
+ format_str.format(idx, len(seg.data), seg.addr, seg.file_offs, seg_name)
+ )
+ print()
+
+ # Footer
+ title = f"{args.chip.upper()} image footer"
+ print(title)
+ print("=" * len(title))
+ calc_checksum = image.calculate_checksum()
+ print(
+ "Checksum: {:#02x} ({})".format(
+ image.checksum,
+ "valid"
+ if image.checksum == calc_checksum
+ else "invalid - calculated {:02x}".format(calc_checksum),
+ )
+ )
+ try:
+ digest_msg = "Not appended"
+ if image.append_digest:
+ is_valid = image.stored_digest == image.calc_digest
+ digest_msg = "{} ({})".format(
+ hexify(image.calc_digest, uppercase=False),
+ "valid" if is_valid else "invalid",
+ )
+ print("Validation hash: {}".format(digest_msg))
+ except AttributeError:
+ pass # ESP8266 image has no append_digest field
+
+ if app_desc:
+ APP_DESC_STRUCT_FMT = " 1 else ""))
+
+ image.verify()
+
+ if args.output is None:
+ args.output = image.default_output_name(args.input)
+ image.save(args.output)
+
+ print("Successfully created {} image.".format(args.chip))
+
+
+def read_mac(esp, args):
+ mac = esp.read_mac()
+
+ def print_mac(label, mac):
+ print("%s: %s" % (label, ":".join(map(lambda x: "%02x" % x, mac))))
+
+ print_mac("MAC", mac)
+
+
+def chip_id(esp, args):
+ try:
+ chipid = esp.chip_id()
+ print("Chip ID: 0x%08x" % chipid)
+ except NotSupportedError:
+ print("Warning: %s has no Chip ID. Reading MAC instead." % esp.CHIP_NAME)
+ read_mac(esp, args)
+
+
+def erase_flash(esp, args):
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ if esp.get_flash_encryption_enabled() or esp.get_secure_boot_enabled():
+ raise FatalError(
+ "Active security features detected, "
+ "erasing flash is disabled as a safety measure. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ print("Erasing flash (this may take a while)...")
+ t = time.time()
+ esp.erase_flash()
+ print("Chip erase completed successfully in %.1fs" % (time.time() - t))
+
+
+def erase_region(esp, args):
+ if not args.force and esp.CHIP_NAME != "ESP8266" and not esp.secure_download_mode:
+ if esp.get_flash_encryption_enabled() or esp.get_secure_boot_enabled():
+ raise FatalError(
+ "Active security features detected, "
+ "erasing flash is disabled as a safety measure. "
+ "Use --force to override, "
+ "please use with caution, otherwise it may brick your device!"
+ )
+ print("Erasing region (may be slow depending on size)...")
+ t = time.time()
+ esp.erase_region(args.address, args.size)
+ print("Erase completed successfully in %.1f seconds." % (time.time() - t))
+
+
+def run(esp, args):
+ esp.run()
+
+
+def flash_id(esp, args):
+ flash_id = esp.flash_id()
+ print("Manufacturer: %02x" % (flash_id & 0xFF))
+ flid_lowbyte = (flash_id >> 16) & 0xFF
+ print("Device: %02x%02x" % ((flash_id >> 8) & 0xFF, flid_lowbyte))
+ print(
+ "Detected flash size: %s" % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))
+ )
+ flash_type = esp.flash_type()
+ flash_type_dict = {0: "quad (4 data lines)", 1: "octal (8 data lines)"}
+ flash_type_str = flash_type_dict.get(flash_type)
+ if flash_type_str:
+ print(f"Flash type set in eFuse: {flash_type_str}")
+
+
+def read_flash(esp, args):
+ if args.no_progress:
+ flash_progress = None
+ else:
+
+ def flash_progress(progress, length):
+ msg = "%d (%d %%)" % (progress, progress * 100.0 / length)
+ padding = "\b" * len(msg)
+ if progress == length:
+ padding = "\n"
+ sys.stdout.write(msg + padding)
+ sys.stdout.flush()
+
+ t = time.time()
+ data = esp.read_flash(args.address, args.size, flash_progress)
+ t = time.time() - t
+ speed_msg = " ({:.1f} kbit/s)".format(len(data) / t * 8 / 1000) if t > 0.0 else ""
+ print_overwrite(
+ "Read {:d} bytes at {:#010x} in {:.1f} seconds{}...".format(
+ len(data), args.address, t, speed_msg
+ ),
+ last_line=True,
+ )
+ with open(args.filename, "wb") as f:
+ f.write(data)
+
+
+def verify_flash(esp, args):
+ differences = False
+
+ for address, argfile in args.addr_filename:
+ image = pad_to(argfile.read(), 4)
+ argfile.seek(0) # rewind in case we need it again
+
+ image = _update_image_flash_params(esp, address, args, image)
+
+ image_size = len(image)
+ print(
+ "Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s..."
+ % (image_size, image_size, address, argfile.name)
+ )
+ # Try digest first, only read if there are differences.
+ digest = esp.flash_md5sum(address, image_size)
+ expected_digest = hashlib.md5(image).hexdigest()
+ if digest == expected_digest:
+ print("-- verify OK (digest matched)")
+ continue
+ else:
+ differences = True
+ if getattr(args, "diff", "no") != "yes":
+ print("-- verify FAILED (digest mismatch)")
+ continue
+
+ flash = esp.read_flash(address, image_size)
+ assert flash != image
+ diff = [i for i in range(image_size) if flash[i] != image[i]]
+ print(
+ "-- verify FAILED: %d differences, first @ 0x%08x"
+ % (len(diff), address + diff[0])
+ )
+ for d in diff:
+ flash_byte = flash[d]
+ image_byte = image[d]
+ print(" %08x %02x %02x" % (address + d, flash_byte, image_byte))
+ if differences:
+ raise FatalError("Verify failed.")
+
+
+def read_flash_status(esp, args):
+ print("Status value: 0x%04x" % esp.read_status(args.bytes))
+
+
+def write_flash_status(esp, args):
+ fmt = "0x%%0%dx" % (args.bytes * 2)
+ args.value = args.value & ((1 << (args.bytes * 8)) - 1)
+ print(("Initial flash status: " + fmt) % esp.read_status(args.bytes))
+ print(("Setting flash status: " + fmt) % args.value)
+ esp.write_status(args.value, args.bytes, args.non_volatile)
+ print(("After flash status: " + fmt) % esp.read_status(args.bytes))
+
+
+def get_security_info(esp, args):
+ si = esp.get_security_info()
+ # TODO: better display
+ print("Flags: {:#010x} ({})".format(si["flags"], bin(si["flags"])))
+ print("Flash_Crypt_Cnt: {:#x}".format(si["flash_crypt_cnt"]))
+ print("Key_Purposes: {}".format(si["key_purposes"]))
+ if si["chip_id"] is not None and si["api_version"] is not None:
+ print("Chip_ID: {}".format(si["chip_id"]))
+ print("Api_Version: {}".format(si["api_version"]))
+
+
+def merge_bin(args):
+ try:
+ chip_class = CHIP_DEFS[args.chip]
+ except KeyError:
+ msg = (
+ "Please specify the chip argument"
+ if args.chip == "auto"
+ else "Invalid chip choice: '{}'".format(args.chip)
+ )
+ msg = msg + " (choose from {})".format(", ".join(CHIP_LIST))
+ raise FatalError(msg)
+
+ # sort the files by offset.
+ # The AddrFilenamePairAction has already checked for overlap
+ input_files = sorted(args.addr_filename, key=lambda x: x[0])
+ if not input_files:
+ raise FatalError("No input files specified")
+ first_addr = input_files[0][0]
+ if first_addr < args.target_offset:
+ raise FatalError(
+ "Output file target offset is 0x%x. Input file offset 0x%x is before this."
+ % (args.target_offset, first_addr)
+ )
+
+ if args.format != "raw":
+ raise FatalError(
+ "This version of esptool only supports the 'raw' output format"
+ )
+
+ with open(args.output, "wb") as of:
+
+ def pad_to(flash_offs):
+ # account for output file offset if there is any
+ of.write(b"\xFF" * (flash_offs - args.target_offset - of.tell()))
+
+ for addr, argfile in input_files:
+ pad_to(addr)
+ image = argfile.read()
+ image = _update_image_flash_params(chip_class, addr, args, image)
+ of.write(image)
+ if args.fill_flash_size:
+ pad_to(flash_size_bytes(args.fill_flash_size))
+ print(
+ "Wrote 0x%x bytes to file %s, ready to flash to offset 0x%x"
+ % (of.tell(), args.output, args.target_offset)
+ )
+
+
+def version(args):
+ from . import __version__
+
+ print(__version__)
diff --git a/installer/bin/esptool/esptool/config.py b/installer/bin/esptool/esptool/config.py
new file mode 100644
index 0000000..5566bec
--- /dev/null
+++ b/installer/bin/esptool/esptool/config.py
@@ -0,0 +1,92 @@
+# SPDX-FileCopyrightText: 2014-2023 Espressif Systems (Shanghai) CO LTD,
+# other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import configparser
+import os
+
+CONFIG_OPTIONS = [
+ "timeout",
+ "chip_erase_timeout",
+ "max_timeout",
+ "sync_timeout",
+ "md5_timeout_per_mb",
+ "erase_region_timeout_per_mb",
+ "erase_write_timeout_per_mb",
+ "mem_end_rom_timeout",
+ "serial_write_timeout",
+ "connect_attempts",
+ "write_block_attempts",
+ "reset_delay",
+ "custom_reset_sequence",
+]
+
+
+def _validate_config_file(file_path, verbose=False):
+ if not os.path.exists(file_path):
+ return False
+
+ cfg = configparser.RawConfigParser()
+ try:
+ cfg.read(file_path, encoding="UTF-8")
+ # Only consider it a valid config file if it contains [esptool] section
+ if cfg.has_section("esptool"):
+ if verbose:
+ unknown_opts = list(set(cfg.options("esptool")) - set(CONFIG_OPTIONS))
+ unknown_opts.sort()
+ no_of_unknown_opts = len(unknown_opts)
+ if no_of_unknown_opts > 0:
+ suffix = "s" if no_of_unknown_opts > 1 else ""
+ print(
+ "Ignoring unknown config file option{}: {}".format(
+ suffix, ", ".join(unknown_opts)
+ )
+ )
+ return True
+ except (UnicodeDecodeError, configparser.Error) as e:
+ if verbose:
+ print(f"Ignoring invalid config file {file_path}: {e}")
+ return False
+
+
+def _find_config_file(dir_path, verbose=False):
+ for candidate in ("esptool.cfg", "setup.cfg", "tox.ini"):
+ cfg_path = os.path.join(dir_path, candidate)
+ if _validate_config_file(cfg_path, verbose):
+ return cfg_path
+ return None
+
+
+def load_config_file(verbose=False):
+ set_with_env_var = False
+ env_var_path = os.environ.get("ESPTOOL_CFGFILE")
+ if env_var_path is not None and _validate_config_file(env_var_path):
+ cfg_file_path = env_var_path
+ set_with_env_var = True
+ else:
+ home_dir = os.path.expanduser("~")
+ os_config_dir = (
+ f"{home_dir}/.config/esptool"
+ if os.name == "posix"
+ else f"{home_dir}/AppData/Local/esptool/"
+ )
+ # Search priority: 1) current dir, 2) OS specific config dir, 3) home dir
+ for dir_path in (os.getcwd(), os_config_dir, home_dir):
+ cfg_file_path = _find_config_file(dir_path, verbose)
+ if cfg_file_path:
+ break
+
+ cfg = configparser.ConfigParser()
+ cfg["esptool"] = {} # Create an empty esptool config for when no file is found
+
+ if cfg_file_path is not None:
+ # If config file is found and validated, read and parse it
+ cfg.read(cfg_file_path)
+ if verbose:
+ msg = " (set with ESPTOOL_CFGFILE)" if set_with_env_var else ""
+ print(
+ f"Loaded custom configuration from "
+ f"{os.path.abspath(cfg_file_path)}{msg}"
+ )
+ return cfg, cfg_file_path
diff --git a/installer/bin/esptool/esptool/loader.py b/installer/bin/esptool/esptool/loader.py
new file mode 100644
index 0000000..eb64023
--- /dev/null
+++ b/installer/bin/esptool/esptool/loader.py
@@ -0,0 +1,1608 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import base64
+import hashlib
+import itertools
+import json
+import os
+import re
+import string
+import struct
+import sys
+import time
+
+from .config import load_config_file
+from .reset import (
+ ClassicReset,
+ CustomReset,
+ DEFAULT_RESET_DELAY,
+ HardReset,
+ USBJTAGSerialReset,
+ UnixTightReset,
+)
+from .util import FatalError, NotImplementedInROMError, UnsupportedCommandError
+from .util import byte, hexify, mask_to_shift, pad_to, strip_chip_name
+
+try:
+ import serial
+except ImportError:
+ print(
+ "Pyserial is not installed for %s. "
+ "Check the README for installation instructions." % (sys.executable)
+ )
+ raise
+
+# check 'serial' is 'pyserial' and not 'serial'
+# ref. https://github.com/espressif/esptool/issues/269
+try:
+ if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__:
+ raise ImportError(
+ "esptool.py depends on pyserial, but there is a conflict with a currently "
+ "installed package named 'serial'.\n"
+ "You may work around this by 'pip uninstall serial; pip install pyserial' "
+ "but this may break other installed Python software "
+ "that depends on 'serial'.\n"
+ "There is no good fix for this right now, "
+ "apart from configuring virtualenvs. "
+ "See https://github.com/espressif/esptool/issues/269#issuecomment-385298196"
+ " for discussion of the underlying issue(s)."
+ )
+except TypeError:
+ pass # __doc__ returns None for pyserial
+
+try:
+ import serial.tools.list_ports as list_ports
+except ImportError:
+ print(
+ "The installed version (%s) of pyserial appears to be too old for esptool.py "
+ "(Python interpreter %s). Check the README for installation instructions."
+ % (sys.VERSION, sys.executable)
+ )
+ raise
+except Exception:
+ if sys.platform == "darwin":
+ # swallow the exception, this is a known issue in pyserial+macOS Big Sur preview
+ # ref https://github.com/espressif/esptool/issues/540
+ list_ports = None
+ else:
+ raise
+
+
+cfg, _ = load_config_file()
+cfg = cfg["esptool"]
+
+# Timeout for most flash operations
+DEFAULT_TIMEOUT = cfg.getfloat("timeout", 3)
+# Timeout for full chip erase
+CHIP_ERASE_TIMEOUT = cfg.getfloat("chip_erase_timeout", 120)
+# Longest any command can run
+MAX_TIMEOUT = cfg.getfloat("max_timeout", CHIP_ERASE_TIMEOUT * 2)
+# Timeout for syncing with bootloader
+SYNC_TIMEOUT = cfg.getfloat("sync_timeout", 0.1)
+# Timeout (per megabyte) for calculating md5sum
+MD5_TIMEOUT_PER_MB = cfg.getfloat("md5_timeout_per_mb", 8)
+# Timeout (per megabyte) for erasing a region
+ERASE_REGION_TIMEOUT_PER_MB = cfg.getfloat("erase_region_timeout_per_mb", 30)
+# Timeout (per megabyte) for erasing and writing data
+ERASE_WRITE_TIMEOUT_PER_MB = cfg.getfloat("erase_write_timeout_per_mb", 40)
+# Short timeout for ESP_MEM_END, as it may never respond
+MEM_END_ROM_TIMEOUT = cfg.getfloat("mem_end_rom_timeout", 0.2)
+# Timeout for serial port write
+DEFAULT_SERIAL_WRITE_TIMEOUT = cfg.getfloat("serial_write_timeout", 10)
+# Default number of times to try connection
+DEFAULT_CONNECT_ATTEMPTS = cfg.getint("connect_attempts", 7)
+# Number of times to try writing a data block
+WRITE_BLOCK_ATTEMPTS = cfg.getint("write_block_attempts", 3)
+
+STUBS_DIR = os.path.join(os.path.dirname(__file__), "./targets/stub_flasher/")
+
+
+def get_stub_json_path(chip_name):
+ chip_name = strip_chip_name(chip_name)
+ chip_name = chip_name.replace("esp", "")
+ return STUBS_DIR + "stub_flasher_" + chip_name + ".json"
+
+
+def timeout_per_mb(seconds_per_mb, size_bytes):
+ """Scales timeouts which are size-specific"""
+ result = seconds_per_mb * (size_bytes / 1e6)
+ if result < DEFAULT_TIMEOUT:
+ return DEFAULT_TIMEOUT
+ return result
+
+
+def check_supported_function(func, check_func):
+ """
+ Decorator implementation that wraps a check around an ESPLoader
+ bootloader function to check if it's supported.
+
+ This is used to capture the multidimensional differences in
+ functionality between the ESP8266 & ESP32 (and later chips) ROM loaders, and the
+ software stub that runs on these. Not possible to do this cleanly
+ via inheritance alone.
+ """
+
+ def inner(*args, **kwargs):
+ obj = args[0]
+ if check_func(obj):
+ return func(*args, **kwargs)
+ else:
+ raise NotImplementedInROMError(obj, func)
+
+ return inner
+
+
+def stub_function_only(func):
+ """Attribute for a function only supported in the software stub loader"""
+ return check_supported_function(func, lambda o: o.IS_STUB)
+
+
+def stub_and_esp32_function_only(func):
+ """Attribute for a function only supported by stubs or ESP32 and later chips ROM"""
+ return check_supported_function(
+ func, lambda o: o.IS_STUB or o.CHIP_NAME not in ["ESP8266"]
+ )
+
+
+def esp32s3_or_newer_function_only(func):
+ """Attribute for a function only supported by ESP32S3 and later chips ROM"""
+ return check_supported_function(
+ func, lambda o: o.CHIP_NAME not in ["ESP8266", "ESP32", "ESP32-S2"]
+ )
+
+
+class StubFlasher:
+ def __init__(self, json_path):
+ with open(json_path) as json_file:
+ stub = json.load(json_file)
+
+ self.text = base64.b64decode(stub["text"])
+ self.text_start = stub["text_start"]
+ self.entry = stub["entry"]
+
+ try:
+ self.data = base64.b64decode(stub["data"])
+ self.data_start = stub["data_start"]
+ except KeyError:
+ self.data = None
+ self.data_start = None
+
+
+class ESPLoader(object):
+ """Base class providing access to ESP ROM & software stub bootloaders.
+ Subclasses provide ESP8266 & ESP32 Family specific functionality.
+
+ Don't instantiate this base class directly, either instantiate a subclass or
+ call cmds.detect_chip() which will interrogate the chip and return the
+ appropriate subclass instance.
+
+ """
+
+ CHIP_NAME = "Espressif device"
+ IS_STUB = False
+
+ FPGA_SLOW_BOOT = False
+
+ DEFAULT_PORT = "/dev/ttyUSB0"
+
+ USES_RFC2217 = False
+
+ # Commands supported by ESP8266 ROM bootloader
+ ESP_FLASH_BEGIN = 0x02
+ ESP_FLASH_DATA = 0x03
+ ESP_FLASH_END = 0x04
+ ESP_MEM_BEGIN = 0x05
+ ESP_MEM_END = 0x06
+ ESP_MEM_DATA = 0x07
+ ESP_SYNC = 0x08
+ ESP_WRITE_REG = 0x09
+ ESP_READ_REG = 0x0A
+
+ # Some comands supported by ESP32 and later chips ROM bootloader (or -8266 w/ stub)
+ ESP_SPI_SET_PARAMS = 0x0B
+ ESP_SPI_ATTACH = 0x0D
+ ESP_READ_FLASH_SLOW = 0x0E # ROM only, much slower than the stub flash read
+ ESP_CHANGE_BAUDRATE = 0x0F
+ ESP_FLASH_DEFL_BEGIN = 0x10
+ ESP_FLASH_DEFL_DATA = 0x11
+ ESP_FLASH_DEFL_END = 0x12
+ ESP_SPI_FLASH_MD5 = 0x13
+
+ # Commands supported by ESP32-S2 and later chips ROM bootloader only
+ ESP_GET_SECURITY_INFO = 0x14
+
+ # Some commands supported by stub only
+ ESP_ERASE_FLASH = 0xD0
+ ESP_ERASE_REGION = 0xD1
+ ESP_READ_FLASH = 0xD2
+ ESP_RUN_USER_CODE = 0xD3
+
+ # Flash encryption encrypted data command
+ ESP_FLASH_ENCRYPT_DATA = 0xD4
+
+ # Response code(s) sent by ROM
+ ROM_INVALID_RECV_MSG = 0x05 # response if an invalid message is received
+
+ # Maximum block sized for RAM and Flash writes, respectively.
+ ESP_RAM_BLOCK = 0x1800
+
+ FLASH_WRITE_SIZE = 0x400
+
+ # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want.
+ ESP_ROM_BAUD = 115200
+
+ # First byte of the application image
+ ESP_IMAGE_MAGIC = 0xE9
+
+ # Initial state for the checksum routine
+ ESP_CHECKSUM_MAGIC = 0xEF
+
+ # Flash sector size, minimum unit of erase.
+ FLASH_SECTOR_SIZE = 0x1000
+
+ UART_DATE_REG_ADDR = 0x60000078
+
+ # This ROM address has a different value on each chip model
+ CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000
+
+ UART_CLKDIV_MASK = 0xFFFFF
+
+ # Memory addresses
+ IROM_MAP_START = 0x40200000
+ IROM_MAP_END = 0x40300000
+
+ # The number of bytes in the UART response that signify command status
+ STATUS_BYTES_LENGTH = 2
+
+ # Bootloader flashing offset
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ # ROM supports an encrypted flashing mode
+ SUPPORTS_ENCRYPTED_FLASH = False
+
+ # Response to ESP_SYNC might indicate that flasher stub is running
+ # instead of the ROM bootloader
+ sync_stub_detected = False
+
+ # Device PIDs
+ USB_JTAG_SERIAL_PID = 0x1001
+
+ # Chip IDs that are no longer supported by esptool
+ UNSUPPORTED_CHIPS = {6: "ESP32-S3(beta 3)"}
+
+ def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False):
+ """Base constructor for ESPLoader bootloader interaction
+
+ Don't call this constructor, either instantiate a specific
+ ROM class directly, or use cmds.detect_chip().
+
+ This base class has all of the instance methods for bootloader
+ functionality supported across various chips & stub
+ loaders. Subclasses replace the functions they don't support
+ with ones which throw NotImplementedInROMError().
+
+ """
+ # True if esptool detects the ROM is in Secure Download Mode
+ self.secure_download_mode = False
+ # True if esptool detects conditions which require the stub to be disabled
+ self.stub_is_disabled = False
+
+ # Device-and-runtime-specific cache
+ self.cache = {
+ "flash_id": None,
+ "chip_id": None,
+ "uart_no": None,
+ }
+
+ if isinstance(port, str):
+ try:
+ self._port = serial.serial_for_url(port)
+ except serial.serialutil.SerialException:
+ raise FatalError(f"Could not open {port}, the port doesn't exist")
+ else:
+ self._port = port
+ self._slip_reader = slip_reader(self._port, self.trace)
+ # setting baud rate in a separate step is a workaround for
+ # CH341 driver on some Linux versions (this opens at 9600 then
+ # sets), shouldn't matter for other platforms/drivers. See
+ # https://github.com/espressif/esptool/issues/44#issuecomment-107094446
+ self._set_port_baudrate(baud)
+ self._trace_enabled = trace_enabled
+ # set write timeout, to prevent esptool blocked at write forever.
+ try:
+ self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT
+ except NotImplementedError:
+ # no write timeout for RFC2217 ports
+ # need to set the property back to None or it will continue to fail
+ self._port.write_timeout = None
+
+ @property
+ def serial_port(self):
+ return self._port.port
+
+ def _set_port_baudrate(self, baud):
+ try:
+ self._port.baudrate = baud
+ except IOError:
+ raise FatalError(
+ "Failed to set baud rate %d. The driver may not support this rate."
+ % baud
+ )
+
+ def read(self):
+ """Read a SLIP packet from the serial port"""
+ return next(self._slip_reader)
+
+ def write(self, packet):
+ """Write bytes to the serial port while performing SLIP escaping"""
+ buf = (
+ b"\xc0"
+ + (packet.replace(b"\xdb", b"\xdb\xdd").replace(b"\xc0", b"\xdb\xdc"))
+ + b"\xc0"
+ )
+ self.trace("Write %d bytes: %s", len(buf), HexFormatter(buf))
+ self._port.write(buf)
+
+ def trace(self, message, *format_args):
+ if self._trace_enabled:
+ now = time.time()
+ try:
+ delta = now - self._last_trace
+ except AttributeError:
+ delta = 0.0
+ self._last_trace = now
+ prefix = "TRACE +%.3f " % delta
+ print(prefix + (message % format_args))
+
+ @staticmethod
+ def checksum(data, state=ESP_CHECKSUM_MAGIC):
+ """Calculate checksum of a blob, as it is defined by the ROM"""
+ for b in data:
+ state ^= b
+
+ return state
+
+ def command(
+ self,
+ op=None,
+ data=b"",
+ chk=0,
+ wait_response=True,
+ timeout=DEFAULT_TIMEOUT,
+ ):
+ """Send a request and read the response"""
+ saved_timeout = self._port.timeout
+ new_timeout = min(timeout, MAX_TIMEOUT)
+ if new_timeout != saved_timeout:
+ self._port.timeout = new_timeout
+
+ try:
+ if op is not None:
+ self.trace(
+ "command op=0x%02x data len=%s wait_response=%d "
+ "timeout=%.3f data=%s",
+ op,
+ len(data),
+ 1 if wait_response else 0,
+ timeout,
+ HexFormatter(data),
+ )
+ pkt = struct.pack(b" self.STATUS_BYTES_LENGTH:
+ return data[: -self.STATUS_BYTES_LENGTH]
+ else:
+ # otherwise, just return the 'val' field which comes from the reply header
+ # (this is used by read_reg)
+ return val
+
+ def flush_input(self):
+ self._port.flushInput()
+ self._slip_reader = slip_reader(self._port, self.trace)
+
+ def sync(self):
+ val, _ = self.command(
+ self.ESP_SYNC, b"\x07\x07\x12\x20" + 32 * b"\x55", timeout=SYNC_TIMEOUT
+ )
+
+ # ROM bootloaders send some non-zero "val" response. The flasher stub sends 0.
+ # If we receive 0 then it probably indicates that the chip wasn't or couldn't be
+ # reseted properly and esptool is talking to the flasher stub.
+ self.sync_stub_detected = val == 0
+
+ for _ in range(7):
+ val, _ = self.command()
+ self.sync_stub_detected &= val == 0
+
+ def _get_pid(self):
+ if list_ports is None:
+ print(
+ "\nListing all serial ports is currently not available. "
+ "Can't get device PID."
+ )
+ return
+ active_port = self._port.port
+
+ # Pyserial only identifies regular ports, URL handlers are not supported
+ if not active_port.lower().startswith(("com", "/dev/")):
+ print(
+ "\nDevice PID identification is only supported on "
+ "COM and /dev/ serial ports."
+ )
+ return
+ # Return the real path if the active port is a symlink
+ if active_port.startswith("/dev/") and os.path.islink(active_port):
+ active_port = os.path.realpath(active_port)
+
+ # The "cu" (call-up) device has to be used for outgoing communication on MacOS
+ if sys.platform == "darwin" and "tty" in active_port:
+ active_port = [active_port, active_port.replace("tty", "cu")]
+ ports = list_ports.comports()
+ for p in ports:
+ if p.device in active_port:
+ return p.pid
+ print(
+ "\nFailed to get PID of a device on {}, "
+ "using standard reset sequence.".format(active_port)
+ )
+
+ def _connect_attempt(self, reset_strategy, mode="default_reset"):
+ """A single connection attempt"""
+ last_error = None
+ boot_log_detected = False
+ download_mode = False
+
+ # If we're doing no_sync, we're likely communicating as a pass through
+ # with an intermediate device to the ESP32
+ if mode == "no_reset_no_sync":
+ return last_error
+
+ if mode != "no_reset":
+ if not self.USES_RFC2217: # Might block on rfc2217 ports
+ # Empty serial buffer to isolate boot log
+ self._port.reset_input_buffer()
+
+ reset_strategy() # Reset the chip to bootloader (download mode)
+
+ # Detect the ROM boot log and check actual boot mode (ESP32 and later only)
+ waiting = self._port.inWaiting()
+ read_bytes = self._port.read(waiting)
+ data = re.search(
+ b"boot:(0x[0-9a-fA-F]+)(.*waiting for download)?", read_bytes, re.DOTALL
+ )
+ if data is not None:
+ boot_log_detected = True
+ boot_mode = data.group(1)
+ download_mode = data.group(2) is not None
+
+ for _ in range(5):
+ try:
+ self.flush_input()
+ self._port.flushOutput()
+ self.sync()
+ return None
+ except FatalError as e:
+ print(".", end="")
+ sys.stdout.flush()
+ time.sleep(0.05)
+ last_error = e
+
+ if boot_log_detected:
+ last_error = FatalError(
+ "Wrong boot mode detected ({})! "
+ "The chip needs to be in download mode.".format(
+ boot_mode.decode("utf-8")
+ )
+ )
+ if download_mode:
+ last_error = FatalError(
+ "Download mode successfully detected, but getting no sync reply: "
+ "The serial TX path seems to be down."
+ )
+ return last_error
+
+ def get_memory_region(self, name):
+ """
+ Returns a tuple of (start, end) for the memory map entry with the given name,
+ or None if it doesn't exist
+ """
+ try:
+ return [(start, end) for (start, end, n) in self.MEMORY_MAP if n == name][0]
+ except IndexError:
+ return None
+
+ def _construct_reset_strategy_sequence(self, mode):
+ """
+ Constructs a sequence of reset strategies based on the OS,
+ used ESP chip, external settings, and environment variables.
+ Returns a tuple of one or more reset strategies to be tried sequentially.
+ """
+ cfg_custom_reset_sequence = cfg.get("custom_reset_sequence")
+ if cfg_custom_reset_sequence is not None:
+ return (CustomReset(self._port, cfg_custom_reset_sequence),)
+
+ cfg_reset_delay = cfg.getfloat("reset_delay")
+ if cfg_reset_delay is not None:
+ delay = extra_delay = cfg_reset_delay
+ else:
+ delay = DEFAULT_RESET_DELAY
+ extra_delay = DEFAULT_RESET_DELAY + 0.5
+
+ # This FPGA delay is for Espressif internal use
+ if (
+ self.FPGA_SLOW_BOOT
+ and os.environ.get("ESPTOOL_ENV_FPGA", "").strip() == "1"
+ ):
+ delay = extra_delay = 7
+
+ # USB-JTAG/Serial mode
+ if mode == "usb_reset" or self._get_pid() == self.USB_JTAG_SERIAL_PID:
+ return (USBJTAGSerialReset(self._port),)
+
+ # USB-to-Serial bridge
+ if os.name != "nt" and not self._port.name.startswith("rfc2217:"):
+ return (
+ UnixTightReset(self._port, delay),
+ UnixTightReset(self._port, extra_delay),
+ ClassicReset(self._port, delay),
+ ClassicReset(self._port, extra_delay),
+ )
+
+ return (
+ ClassicReset(self._port, delay),
+ ClassicReset(self._port, extra_delay),
+ )
+
+ def connect(
+ self,
+ mode="default_reset",
+ attempts=DEFAULT_CONNECT_ATTEMPTS,
+ detecting=False,
+ warnings=True,
+ ):
+ """Try connecting repeatedly until successful, or giving up"""
+ if warnings and mode in ["no_reset", "no_reset_no_sync"]:
+ print(
+ 'WARNING: Pre-connection option "{}" was selected.'.format(mode),
+ "Connection may fail if the chip is not in bootloader "
+ "or flasher stub mode.",
+ )
+ print("Connecting...", end="")
+ sys.stdout.flush()
+ last_error = None
+
+ reset_sequence = self._construct_reset_strategy_sequence(mode)
+ try:
+ for _, reset_strategy in zip(
+ range(attempts) if attempts > 0 else itertools.count(),
+ itertools.cycle(reset_sequence),
+ ):
+ last_error = self._connect_attempt(reset_strategy, mode)
+ if last_error is None:
+ break
+ finally:
+ print("") # end 'Connecting...' line
+
+ if last_error is not None:
+ raise FatalError(
+ "Failed to connect to {}: {}"
+ "\nFor troubleshooting steps visit: "
+ "https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html".format( # noqa E501
+ self.CHIP_NAME, last_error
+ )
+ )
+
+ if not detecting:
+ try:
+ from .targets import ROM_LIST
+
+ # check the date code registers match what we expect to see
+ chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR)
+ if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE:
+ actually = None
+ for cls in ROM_LIST:
+ if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
+ actually = cls
+ break
+ if warnings and actually is None:
+ print(
+ "WARNING: This chip doesn't appear to be a %s "
+ "(chip magic value 0x%08x). "
+ "Probably it is unsupported by this version of esptool."
+ % (self.CHIP_NAME, chip_magic_value)
+ )
+ else:
+ raise FatalError(
+ "This chip is %s not %s. Wrong --chip argument?"
+ % (actually.CHIP_NAME, self.CHIP_NAME)
+ )
+ except UnsupportedCommandError:
+ self.secure_download_mode = True
+
+ try:
+ self.check_chip_id()
+ except UnsupportedCommandError:
+ # Fix for ROM not responding in SDM, reconnect and try again
+ if self.secure_download_mode:
+ self._connect_attempt(mode, reset_sequence[0])
+ self.check_chip_id()
+ else:
+ raise
+ self._post_connect()
+
+ def _post_connect(self):
+ """
+ Additional initialization hook, may be overridden by the chip-specific class.
+ Gets called after connect, and after auto-detection.
+ """
+ pass
+
+ def read_reg(self, addr, timeout=DEFAULT_TIMEOUT):
+ """Read memory address in target"""
+ # we don't call check_command here because read_reg() function is called
+ # when detecting chip type, and the way we check for success
+ # (STATUS_BYTES_LENGTH) is different for different chip types (!)
+ val, data = self.command(
+ self.ESP_READ_REG, struct.pack(" 0:
+ # add a dummy write to a date register as an excuse to have a delay
+ command += struct.pack(
+ " start:
+ raise FatalError(
+ "Software loader is resident at 0x%08x-0x%08x. "
+ "Can't load binary at overlapping address range 0x%08x-0x%08x. "
+ "Either change binary loading address, or use the --no-stub "
+ "option to disable the software loader."
+ % (start, end, load_start, load_end)
+ )
+
+ return self.check_command(
+ "enter RAM download mode",
+ self.ESP_MEM_BEGIN,
+ struct.pack(" length:
+ raise FatalError("Read more than expected")
+
+ digest_frame = self.read()
+ if len(digest_frame) != 16:
+ raise FatalError("Expected digest, got: %s" % hexify(digest_frame))
+ expected_digest = hexify(digest_frame).upper()
+ digest = hashlib.md5(data).hexdigest().upper()
+ if digest != expected_digest:
+ raise FatalError(
+ "Digest mismatch: expected %s, got %s" % (expected_digest, digest)
+ )
+ return data
+
+ def flash_spi_attach(self, hspi_arg):
+ """Send SPI attach command to enable the SPI flash pins
+
+ ESP8266 ROM does this when you send flash_begin, ESP32 ROM
+ has it as a SPI command.
+ """
+ # last 3 bytes in ESP_SPI_ATTACH argument are reserved values
+ arg = struct.pack(" 0:
+ self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1)
+ if miso_bits > 0:
+ self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1)
+ flags = 0
+ if dummy_len > 0:
+ flags |= dummy_len - 1
+ if addr_len > 0:
+ flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+ if flags:
+ self.write_reg(SPI_USR1_REG, flags)
+
+ else:
+
+ def set_data_lengths(mosi_bits, miso_bits):
+ SPI_DATA_LEN_REG = SPI_USR1_REG
+ SPI_MOSI_BITLEN_S = 17
+ SPI_MISO_BITLEN_S = 8
+ mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1)
+ miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1)
+ flags = (miso_mask << SPI_MISO_BITLEN_S) | (
+ mosi_mask << SPI_MOSI_BITLEN_S
+ )
+ if dummy_len > 0:
+ flags |= dummy_len - 1
+ if addr_len > 0:
+ flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+ self.write_reg(SPI_DATA_LEN_REG, flags)
+
+ # SPI peripheral "command" bitmasks for SPI_CMD_REG
+ SPI_CMD_USR = 1 << 18
+
+ # shift values
+ SPI_USR2_COMMAND_LEN_SHIFT = 28
+ SPI_USR_ADDR_LEN_SHIFT = 26
+
+ if read_bits > 32:
+ raise FatalError(
+ "Reading more than 32 bits back from a SPI flash "
+ "operation is unsupported"
+ )
+ if len(data) > 64:
+ raise FatalError(
+ "Writing more than 64 bytes of data with one SPI "
+ "command is unsupported"
+ )
+
+ data_bits = len(data) * 8
+ old_spi_usr = self.read_reg(SPI_USR_REG)
+ old_spi_usr2 = self.read_reg(SPI_USR2_REG)
+ flags = SPI_USR_COMMAND
+ if read_bits > 0:
+ flags |= SPI_USR_MISO
+ if data_bits > 0:
+ flags |= SPI_USR_MOSI
+ if addr_len > 0:
+ flags |= SPI_USR_ADDR
+ if dummy_len > 0:
+ flags |= SPI_USR_DUMMY
+ set_data_lengths(data_bits, read_bits)
+ self.write_reg(SPI_USR_REG, flags)
+ self.write_reg(
+ SPI_USR2_REG, (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command
+ )
+ if addr and addr_len > 0:
+ self.write_reg(SPI_ADDR_REG, addr)
+ if data_bits == 0:
+ self.write_reg(SPI_W0_REG, 0) # clear data register before we read it
+ else:
+ data = pad_to(data, 4, b"\00") # pad to 32-bit multiple
+ words = struct.unpack("I" * (len(data) // 4), data)
+ next_reg = SPI_W0_REG
+ for word in words:
+ self.write_reg(next_reg, word)
+ next_reg += 4
+ self.write_reg(SPI_CMD_REG, SPI_CMD_USR)
+
+ def wait_done():
+ for _ in range(10):
+ if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0:
+ return
+ raise FatalError("SPI command did not complete in time")
+
+ wait_done()
+
+ status = self.read_reg(SPI_W0_REG)
+ # restore some SPI controller registers
+ self.write_reg(SPI_USR_REG, old_spi_usr)
+ self.write_reg(SPI_USR2_REG, old_spi_usr2)
+ return status
+
+ def read_spiflash_sfdp(self, addr, read_bits):
+ CMD_RDSFDP = 0x5A
+ return self.run_spiflash_command(
+ CMD_RDSFDP, read_bits=read_bits, addr=addr, addr_len=24, dummy_len=8
+ )
+
+ def read_status(self, num_bytes=2):
+ """Read up to 24 bits (num_bytes) of SPI flash status register contents
+ via RDSR, RDSR2, RDSR3 commands
+
+ Not all SPI flash supports all three commands. The upper 1 or 2
+ bytes may be 0xFF.
+ """
+ SPIFLASH_RDSR = 0x05
+ SPIFLASH_RDSR2 = 0x35
+ SPIFLASH_RDSR3 = 0x15
+
+ status = 0
+ shift = 0
+ for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]:
+ status += self.run_spiflash_command(cmd, read_bits=8) << shift
+ shift += 8
+ return status
+
+ def write_status(self, new_status, num_bytes=2, set_non_volatile=False):
+ """Write up to 24 bits (num_bytes) of new status register
+
+ num_bytes can be 1, 2 or 3.
+
+ Not all flash supports the additional commands to write the
+ second and third byte of the status register. When writing 2
+ bytes, esptool also sends a 16-byte WRSR command (as some
+ flash types use this instead of WRSR2.)
+
+ If the set_non_volatile flag is set, non-volatile bits will
+ be set as well as volatile ones (WREN used instead of WEVSR).
+
+ """
+ SPIFLASH_WRSR = 0x01
+ SPIFLASH_WRSR2 = 0x31
+ SPIFLASH_WRSR3 = 0x11
+ SPIFLASH_WEVSR = 0x50
+ SPIFLASH_WREN = 0x06
+ SPIFLASH_WRDI = 0x04
+
+ enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR
+
+ # try using a 16-bit WRSR (not supported by all chips)
+ # this may be redundant, but shouldn't hurt
+ if num_bytes == 2:
+ self.run_spiflash_command(enable_cmd)
+ self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8
+
+ self.run_spiflash_command(SPIFLASH_WRDI)
+
+ def get_crystal_freq(self):
+ """
+ Figure out the crystal frequency from the UART clock divider
+
+ Returns a normalized value in integer MHz (only values 40 or 26 are supported)
+ """
+ # The logic here is:
+ # - We know that our baud rate and the ESP UART baud rate are roughly the same,
+ # or we couldn't communicate
+ # - We can read the UART clock divider register to know how the ESP derives this
+ # from the APB bus frequency
+ # - Multiplying these two together gives us the bus frequency which is either
+ # the crystal frequency (ESP32) or double the crystal frequency (ESP8266).
+ # See the self.XTAL_CLK_DIVIDER parameter for this factor.
+ uart_div = self.read_reg(self.UART_CLKDIV_REG) & self.UART_CLKDIV_MASK
+ est_xtal = (self._port.baudrate * uart_div) / 1e6 / self.XTAL_CLK_DIVIDER
+ norm_xtal = 40 if est_xtal > 33 else 26
+ if abs(norm_xtal - est_xtal) > 1:
+ print(
+ "WARNING: Detected crystal freq %.2fMHz is quite different to "
+ "normalized freq %dMHz. Unsupported crystal in use?"
+ % (est_xtal, norm_xtal)
+ )
+ return norm_xtal
+
+ def hard_reset(self):
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port)()
+
+ def soft_reset(self, stay_in_bootloader):
+ if not self.IS_STUB:
+ if stay_in_bootloader:
+ return # ROM bootloader is already in bootloader!
+ else:
+ # 'run user code' is as close to a soft reset as we can do
+ self.flash_begin(0, 0)
+ self.flash_finish(False)
+ else:
+ if stay_in_bootloader:
+ # soft resetting from the stub loader
+ # will re-load the ROM bootloader
+ self.flash_begin(0, 0)
+ self.flash_finish(True)
+ elif self.CHIP_NAME != "ESP8266":
+ raise FatalError(
+ "Soft resetting is currently only supported on ESP8266"
+ )
+ else:
+ # running user code from stub loader requires some hacks
+ # in the stub loader
+ self.command(self.ESP_RUN_USER_CODE, wait_response=False)
+
+ def check_chip_id(self):
+ try:
+ chip_id = self.get_chip_id()
+ if chip_id != self.IMAGE_CHIP_ID:
+ print(
+ "WARNING: Chip ID {} ({}) doesn't match expected Chip ID {}. "
+ "esptool may not work correctly.".format(
+ chip_id,
+ self.UNSUPPORTED_CHIPS.get(chip_id, "Unknown"),
+ self.IMAGE_CHIP_ID,
+ )
+ )
+ # Try to flash anyways by disabling stub
+ self.stub_is_disabled = True
+ except NotImplementedInROMError:
+ pass
+
+
+def slip_reader(port, trace_function):
+ """Generator to read SLIP packets from a serial port.
+ Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
+
+ Designed to avoid too many calls to serial.read(1), which can bog
+ down on slow systems.
+ """
+
+ def detect_panic_handler(input):
+ """
+ Checks the input bytes for panic handler messages.
+ Raises a FatalError if Guru Meditation or Fatal Exception is found, as both
+ of these are used between different ROM versions.
+ Tries to also parse the error cause (e.g. IllegalInstruction).
+ """
+
+ guru_meditation = (
+ rb"G?uru Meditation Error: (?:Core \d panic'ed \(([a-zA-Z ]*)\))?"
+ )
+ fatal_exception = rb"F?atal exception \(\d+\): (?:([a-zA-Z ]*)?.*epc)?"
+
+ # Search either for Guru Meditation or Fatal Exception
+ data = re.search(
+ rb"".join([rb"(?:", guru_meditation, rb"|", fatal_exception, rb")"]),
+ input,
+ re.DOTALL,
+ )
+ if data is not None:
+ cause = [
+ "({})".format(i.decode("utf-8"))
+ for i in [data.group(1), data.group(2)]
+ if i is not None
+ ]
+ cause = f" {cause[0]}" if len(cause) else ""
+ msg = f"Guru Meditation Error detected{cause}"
+ raise FatalError(msg)
+
+ partial_packet = None
+ in_escape = False
+ successful_slip = False
+ while True:
+ waiting = port.inWaiting()
+ read_bytes = port.read(1 if waiting == 0 else waiting)
+ if read_bytes == b"":
+ if partial_packet is None: # fail due to no data
+ msg = (
+ "Serial data stream stopped: Possible serial noise or corruption."
+ if successful_slip
+ else "No serial data received."
+ )
+ else: # fail during packet transfer
+ msg = "Packet content transfer stopped (received {} bytes)".format(
+ len(partial_packet)
+ )
+ trace_function(msg)
+ raise FatalError(msg)
+ trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes))
+ for b in read_bytes:
+ b = bytes([b])
+ if partial_packet is None: # waiting for packet header
+ if b == b"\xc0":
+ partial_packet = b""
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ remaining_data = port.read(port.inWaiting())
+ trace_function(
+ "Remaining data in serial buffer: %s",
+ HexFormatter(remaining_data),
+ )
+ detect_panic_handler(read_bytes + remaining_data)
+ raise FatalError(
+ "Invalid head of packet (0x%s): "
+ "Possible serial noise or corruption." % hexify(b)
+ )
+ elif in_escape: # part-way through escape sequence
+ in_escape = False
+ if b == b"\xdc":
+ partial_packet += b"\xc0"
+ elif b == b"\xdd":
+ partial_packet += b"\xdb"
+ else:
+ trace_function("Read invalid data: %s", HexFormatter(read_bytes))
+ remaining_data = port.read(port.inWaiting())
+ trace_function(
+ "Remaining data in serial buffer: %s",
+ HexFormatter(remaining_data),
+ )
+ detect_panic_handler(read_bytes + remaining_data)
+ raise FatalError("Invalid SLIP escape (0xdb, 0x%s)" % (hexify(b)))
+ elif b == b"\xdb": # start of escape sequence
+ in_escape = True
+ elif b == b"\xc0": # end of packet
+ trace_function("Received full packet: %s", HexFormatter(partial_packet))
+ yield partial_packet
+ partial_packet = None
+ successful_slip = True
+ else: # normal byte in packet
+ partial_packet += b
+
+
+class HexFormatter(object):
+ """
+ Wrapper class which takes binary data in its constructor
+ and returns a hex string as it's __str__ method.
+
+ This is intended for "lazy formatting" of trace() output
+ in hex format. Avoids overhead (significant on slow computers)
+ of generating long hex strings even if tracing is disabled.
+
+ Note that this doesn't save any overhead if passed as an
+ argument to "%", only when passed to trace()
+
+ If auto_split is set (default), any long line (> 16 bytes) will be
+ printed as separately indented lines, with ASCII decoding at the end
+ of each line.
+ """
+
+ def __init__(self, binary_string, auto_split=True):
+ self._s = binary_string
+ self._auto_split = auto_split
+
+ def __str__(self):
+ if self._auto_split and len(self._s) > 16:
+ result = ""
+ s = self._s
+ while len(s) > 0:
+ line = s[:16]
+ ascii_line = "".join(
+ c
+ if (
+ c == " "
+ or (c in string.printable and c not in string.whitespace)
+ )
+ else "."
+ for c in line.decode("ascii", "replace")
+ )
+ s = s[16:]
+ result += "\n %-16s %-16s | %s" % (
+ hexify(line[:8], False),
+ hexify(line[8:], False),
+ ascii_line,
+ )
+ return result
+ else:
+ return hexify(self._s, False)
diff --git a/installer/bin/esptool/esptool/reset.py b/installer/bin/esptool/esptool/reset.py
new file mode 100644
index 0000000..39b9a15
--- /dev/null
+++ b/installer/bin/esptool/esptool/reset.py
@@ -0,0 +1,178 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+import time
+
+from .util import FatalError
+
+# Used for resetting into bootloader on Unix-like systems
+if os.name != "nt":
+ import fcntl
+ import termios
+
+ # Constants used for terminal status lines reading/setting.
+ # Taken from pySerial's backend for IO:
+ # https://github.com/pyserial/pyserial/blob/master/serial/serialposix.py
+ TIOCMSET = getattr(termios, "TIOCMSET", 0x5418)
+ TIOCMGET = getattr(termios, "TIOCMGET", 0x5415)
+ TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
+ TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)
+
+DEFAULT_RESET_DELAY = 0.05 # default time to wait before releasing boot pin after reset
+
+
+class ResetStrategy(object):
+ def __init__(self, port, reset_delay=DEFAULT_RESET_DELAY):
+ self.port = port
+ self.reset_delay = reset_delay
+
+ def __call__():
+ pass
+
+ def _setDTR(self, state):
+ self.port.setDTR(state)
+
+ def _setRTS(self, state):
+ self.port.setRTS(state)
+ # Work-around for adapters on Windows using the usbser.sys driver:
+ # generate a dummy change to DTR so that the set-control-line-state
+ # request is sent with the updated RTS state and the same DTR state
+ self.port.setDTR(self.port.dtr)
+
+ def _setDTRandRTS(self, dtr=False, rts=False):
+ status = struct.unpack(
+ "I", fcntl.ioctl(self.port.fileno(), TIOCMGET, struct.pack("I", 0))
+ )[0]
+ if dtr:
+ status |= TIOCM_DTR
+ else:
+ status &= ~TIOCM_DTR
+ if rts:
+ status |= TIOCM_RTS
+ else:
+ status &= ~TIOCM_RTS
+ fcntl.ioctl(self.port.fileno(), TIOCMSET, struct.pack("I", status))
+
+
+class ClassicReset(ResetStrategy):
+ """
+ Classic reset sequence, sets DTR and RTS lines sequentially.
+ """
+
+ def __call__(self):
+ self._setDTR(False) # IO0=HIGH
+ self._setRTS(True) # EN=LOW, chip in reset
+ time.sleep(0.1)
+ self._setDTR(True) # IO0=LOW
+ self._setRTS(False) # EN=HIGH, chip out of reset
+ time.sleep(self.reset_delay)
+ self._setDTR(False) # IO0=HIGH, done
+
+
+class UnixTightReset(ResetStrategy):
+ """
+ UNIX-only reset sequence with custom implementation,
+ which allows setting DTR and RTS lines at the same time.
+ """
+
+ def __call__(self):
+ self._setDTRandRTS(False, False)
+ self._setDTRandRTS(True, True)
+ self._setDTRandRTS(False, True) # IO0=HIGH & EN=LOW, chip in reset
+ time.sleep(0.1)
+ self._setDTRandRTS(True, False) # IO0=LOW & EN=HIGH, chip out of reset
+ time.sleep(self.reset_delay)
+ self._setDTRandRTS(False, False) # IO0=HIGH, done
+ self._setDTR(False) # Needed in some environments to ensure IO0=HIGH
+
+
+class USBJTAGSerialReset(ResetStrategy):
+ """
+ Custom reset sequence, which is required when the device
+ is connecting via its USB-JTAG-Serial peripheral.
+ """
+
+ def __call__(self):
+ self._setRTS(False)
+ self._setDTR(False) # Idle
+ time.sleep(0.1)
+ self._setDTR(True) # Set IO0
+ self._setRTS(False)
+ time.sleep(0.1)
+ self._setRTS(True) # Reset. Calls inverted to go through (1,1) instead of (0,0)
+ self._setDTR(False)
+ self._setRTS(True) # RTS set as Windows only propagates DTR on RTS setting
+ time.sleep(0.1)
+ self._setDTR(False)
+ self._setRTS(False) # Chip out of reset
+
+
+class HardReset(ResetStrategy):
+ """
+ Reset sequence for hard resetting the chip.
+ Can be used to reset out of the bootloader or to restart a running app.
+ """
+
+ def __init__(self, port, uses_usb_otg=False):
+ super().__init__(port)
+ self.uses_usb_otg = uses_usb_otg
+
+ def __call__(self):
+ self._setRTS(True) # EN->LOW
+ if self.uses_usb_otg:
+ # Give the chip some time to come out of reset,
+ # to be able to handle further DTR/RTS transitions
+ time.sleep(0.2)
+ self._setRTS(False)
+ time.sleep(0.2)
+ else:
+ time.sleep(0.1)
+ self._setRTS(False)
+
+
+class CustomReset(ResetStrategy):
+ """
+ Custom reset strategy defined with a string.
+
+ CustomReset object is created as "rst = CustomReset(port, seq_str)"
+ and can be later executed simply with "rst()"
+
+ The seq_str input string consists of individual commands divided by "|".
+ Commands (e.g. R0) are defined by a code (R) and an argument (0).
+
+ The commands are:
+ D: setDTR - 1=True / 0=False
+ R: setRTS - 1=True / 0=False
+ U: setDTRandRTS (Unix-only) - 0,0 / 0,1 / 1,0 / or 1,1
+ W: Wait (time delay) - positive float number
+
+ e.g.
+ "D0|R1|W0.1|D1|R0|W0.05|D0" represents the ClassicReset strategy
+ "U1,1|U0,1|W0.1|U1,0|W0.05|U0,0" represents the UnixTightReset strategy
+ """
+
+ format_dict = {
+ "D": "self.port.setDTR({})",
+ "R": "self.port.setRTS({})",
+ "W": "time.sleep({})",
+ "U": "self._setDTRandRTS({})",
+ }
+
+ def __call__(self):
+ exec(self.constructed_strategy)
+
+ def __init__(self, port, seq_str):
+ super().__init__(port)
+ self.constructed_strategy = self._parse_string_to_seq(seq_str)
+
+ def _parse_string_to_seq(self, seq_str):
+ try:
+ cmds = seq_str.split("|")
+ fn_calls_list = [self.format_dict[cmd[0]].format(cmd[1:]) for cmd in cmds]
+ except Exception as e:
+ raise FatalError(f'Invalid "custom_reset_sequence" option format: {e}')
+ return "\n".join(fn_calls_list)
diff --git a/installer/bin/esptool/esptool/targets/__init__.py b/installer/bin/esptool/esptool/targets/__init__.py
new file mode 100644
index 0000000..9d76ca5
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/__init__.py
@@ -0,0 +1,31 @@
+from .esp32 import ESP32ROM
+from .esp32c2 import ESP32C2ROM
+from .esp32c3 import ESP32C3ROM
+from .esp32c6 import ESP32C6ROM
+from .esp32c6beta import ESP32C6BETAROM
+from .esp32h2 import ESP32H2ROM
+from .esp32h2beta1 import ESP32H2BETA1ROM
+from .esp32h2beta2 import ESP32H2BETA2ROM
+from .esp32s2 import ESP32S2ROM
+from .esp32s3 import ESP32S3ROM
+from .esp32s3beta2 import ESP32S3BETA2ROM
+from .esp8266 import ESP8266ROM
+
+
+CHIP_DEFS = {
+ "esp8266": ESP8266ROM,
+ "esp32": ESP32ROM,
+ "esp32s2": ESP32S2ROM,
+ "esp32s3beta2": ESP32S3BETA2ROM,
+ "esp32s3": ESP32S3ROM,
+ "esp32c3": ESP32C3ROM,
+ "esp32c6beta": ESP32C6BETAROM,
+ "esp32h2beta1": ESP32H2BETA1ROM,
+ "esp32h2beta2": ESP32H2BETA2ROM,
+ "esp32c2": ESP32C2ROM,
+ "esp32c6": ESP32C6ROM,
+ "esp32h2": ESP32H2ROM,
+}
+
+CHIP_LIST = list(CHIP_DEFS.keys())
+ROM_LIST = list(CHIP_DEFS.values())
diff --git a/installer/bin/esptool/esptool/targets/esp32.py b/installer/bin/esptool/esptool/targets/esp32.py
new file mode 100644
index 0000000..b805ed4
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32.py
@@ -0,0 +1,393 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+import time
+
+from ..loader import ESPLoader
+from ..util import FatalError, NotSupportedError
+
+
+class ESP32ROM(ESPLoader):
+ """Access class for ESP32 ROM bootloader"""
+
+ CHIP_NAME = "ESP32"
+ IMAGE_CHIP_ID = 0
+ IS_STUB = False
+
+ FPGA_SLOW_BOOT = True
+
+ CHIP_DETECT_MAGIC_VALUE = [0x00F01D83]
+
+ IROM_MAP_START = 0x400D0000
+ IROM_MAP_END = 0x40400000
+
+ DROM_MAP_START = 0x3F400000
+ DROM_MAP_END = 0x3F800000
+
+ # ESP32 uses a 4 byte status reply
+ STATUS_BYTES_LENGTH = 4
+
+ SPI_REG_BASE = 0x3FF42000
+ SPI_USR_OFFS = 0x1C
+ SPI_USR1_OFFS = 0x20
+ SPI_USR2_OFFS = 0x24
+ SPI_MOSI_DLEN_OFFS = 0x28
+ SPI_MISO_DLEN_OFFS = 0x2C
+ EFUSE_RD_REG_BASE = 0x3FF5A000
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 7 # EFUSE_RD_DISABLE_DL_ENCRYPT
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_RD_REG_BASE # EFUSE_BLK0_WDATA0_REG
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7F << 20 # EFUSE_FLASH_CRYPT_CNT
+
+ EFUSE_RD_ABS_DONE_REG = EFUSE_RD_REG_BASE + 0x018
+ EFUSE_RD_ABS_DONE_0_MASK = 1 << 4
+ EFUSE_RD_ABS_DONE_1_MASK = 1 << 5
+
+ DR_REG_SYSCON_BASE = 0x3FF66000
+ APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C
+ APB_CTL_DATE_V = 0x1
+ APB_CTL_DATE_S = 31
+
+ SPI_W0_OFFS = 0x80
+
+ UART_CLKDIV_REG = 0x3FF40014
+
+ XTAL_CLK_DIVIDER = 1
+
+ RTCCALICFG1 = 0x3FF5F06C
+ TIMERS_RTC_CALI_VALUE = 0x01FFFFFF
+ TIMERS_RTC_CALI_VALUE_S = 7
+
+ FLASH_SIZES = {
+ "1MB": 0x00,
+ "2MB": 0x10,
+ "4MB": 0x20,
+ "8MB": 0x30,
+ "16MB": 0x40,
+ "32MB": 0x50,
+ "64MB": 0x60,
+ "128MB": 0x70,
+ }
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "26m": 0x1,
+ "20m": 0x2,
+ }
+
+ BOOTLOADER_FLASH_OFFSET = 0x1000
+
+ OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"]
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3F400000, 0x3F800000, "DROM"],
+ [0x3F800000, 0x3FC00000, "EXTRAM_DATA"],
+ [0x3FF80000, 0x3FF82000, "RTC_DRAM"],
+ [0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"],
+ [0x3FFAE000, 0x40000000, "DRAM"],
+ [0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"],
+ [0x40000000, 0x40070000, "IROM"],
+ [0x40070000, 0x40078000, "CACHE_PRO"],
+ [0x40078000, 0x40080000, "CACHE_APP"],
+ [0x40080000, 0x400A0000, "IRAM"],
+ [0x400A0000, 0x400BFFFC, "DIRAM_IRAM"],
+ [0x400C0000, 0x400C2000, "RTC_IRAM"],
+ [0x400D0000, 0x40400000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 32
+
+ """ Try to read the BLOCK1 (encryption key) and check if it is valid """
+
+ def is_flash_encryption_key_valid(self):
+ """Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1
+ this bit is at position 16 in EFUSE_BLK0_RDATA0_REG"""
+ word0 = self.read_efuse(0)
+ rd_disable = (word0 >> 16) & 0x1
+
+ # reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed
+ if rd_disable:
+ return True
+ else:
+ # reading of BLOCK1 is ALLOWED so we will read and verify for non-zero.
+ # When ESP32 has not generated AES/encryption key in BLOCK1,
+ # the contents will be readable and 0.
+ # If the flash encryption is enabled it is expected to have a valid
+ # non-zero key. We break out on first occurance of non-zero value
+ key_word = [0] * 7
+ for i in range(len(key_word)):
+ key_word[i] = self.read_efuse(14 + i)
+ # key is non-zero so break & return
+ if key_word[i] != 0:
+ return True
+ return False
+
+ def get_flash_crypt_config(self):
+ """For flash encryption related commands we need to make sure
+ user has programmed all the relevant efuse correctly so before
+ writing encrypted write_flash_encrypt esptool will verify the values
+ of flash_crypt_config to be non zero if they are not read
+ protected. If the values are zero a warning will be printed
+
+ bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config
+ this bit is at position 19 in EFUSE_BLK0_RDATA0_REG"""
+ word0 = self.read_efuse(0)
+ rd_disable = (word0 >> 19) & 0x1
+
+ if rd_disable == 0:
+ """we can read the flash_crypt_config efuse value
+ so go & read it (EFUSE_BLK0_RDATA5_REG[31:28])"""
+ word5 = self.read_efuse(5)
+ word5 = (word5 >> 28) & 0xF
+ return word5
+ else:
+ # if read of the efuse is disabled we assume it is set correctly
+ return 0xF
+
+ def get_encrypted_download_disabled(self):
+ return (
+ self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG)
+ & self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT
+ )
+
+ def get_flash_encryption_enabled(self):
+ flash_crypt_cnt = (
+ self.read_reg(self.EFUSE_SPI_BOOT_CRYPT_CNT_REG)
+ & self.EFUSE_SPI_BOOT_CRYPT_CNT_MASK
+ )
+ # Flash encryption enabled when odd number of bits are set
+ return bin(flash_crypt_cnt).count("1") & 1 != 0
+
+ def get_secure_boot_enabled(self):
+ efuses = self.read_reg(self.EFUSE_RD_ABS_DONE_REG)
+ rev = self.get_chip_revision()
+ return efuses & self.EFUSE_RD_ABS_DONE_0_MASK or (
+ rev >= 300 and efuses & self.EFUSE_RD_ABS_DONE_1_MASK
+ )
+
+ def get_pkg_version(self):
+ word3 = self.read_efuse(3)
+ pkg_version = (word3 >> 9) & 0x07
+ pkg_version += ((word3 >> 2) & 0x1) << 3
+ return pkg_version
+
+ def get_chip_revision(self):
+ return self.get_major_chip_version() * 100 + self.get_minor_chip_version()
+
+ def get_minor_chip_version(self):
+ return (self.read_efuse(5) >> 24) & 0x3
+
+ def get_major_chip_version(self):
+ rev_bit0 = (self.read_efuse(3) >> 15) & 0x1
+ rev_bit1 = (self.read_efuse(5) >> 20) & 0x1
+ apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR)
+ rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V
+ combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0
+
+ revision = {
+ 0: 0,
+ 1: 1,
+ 3: 2,
+ 7: 3,
+ }.get(combine_value, 0)
+ return revision
+
+ def get_chip_description(self):
+ pkg_version = self.get_pkg_version()
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ rev3 = major_rev == 3
+ single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU
+
+ chip_name = {
+ 0: "ESP32-S0WDQ6" if single_core else "ESP32-D0WDQ6",
+ 1: "ESP32-S0WD" if single_core else "ESP32-D0WD",
+ 2: "ESP32-D2WD",
+ 4: "ESP32-U4WDH",
+ 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4",
+ 6: "ESP32-PICO-V3-02",
+ 7: "ESP32-D0WDR2-V3",
+ }.get(pkg_version, "unknown ESP32")
+
+ # ESP32-D0WD-V3, ESP32-D0WDQ6-V3
+ if chip_name.startswith("ESP32-D0WD") and rev3:
+ chip_name += "-V3"
+
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+ word3 = self.read_efuse(3)
+
+ # names of variables in this section are lowercase
+ # versions of EFUSE names as documented in TRM and
+ # ESP-IDF efuse_reg.h
+
+ chip_ver_dis_bt = word3 & (1 << 1)
+ if chip_ver_dis_bt == 0:
+ features += ["BT"]
+
+ chip_ver_dis_app_cpu = word3 & (1 << 0)
+ if chip_ver_dis_app_cpu:
+ features += ["Single Core"]
+ else:
+ features += ["Dual Core"]
+
+ chip_cpu_freq_rated = word3 & (1 << 13)
+ if chip_cpu_freq_rated:
+ chip_cpu_freq_low = word3 & (1 << 12)
+ if chip_cpu_freq_low:
+ features += ["160MHz"]
+ else:
+ features += ["240MHz"]
+
+ pkg_version = self.get_pkg_version()
+ if pkg_version in [2, 4, 5, 6]:
+ features += ["Embedded Flash"]
+
+ if pkg_version == 6:
+ features += ["Embedded PSRAM"]
+
+ word4 = self.read_efuse(4)
+ adc_vref = (word4 >> 8) & 0x1F
+ if adc_vref:
+ features += ["VRef calibration in efuse"]
+
+ blk3_part_res = word3 >> 14 & 0x1
+ if blk3_part_res:
+ features += ["BLK3 partially reserved"]
+
+ word6 = self.read_efuse(6)
+ coding_scheme = word6 & 0x3
+ features += [
+ "Coding Scheme %s"
+ % {0: "None", 1: "3/4", 2: "Repeat (UNSUPPORTED)", 3: "Invalid"}[
+ coding_scheme
+ ]
+ ]
+
+ return features
+
+ def read_efuse(self, n):
+ """Read the nth word of the ESP3x EFUSE region."""
+ return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n))
+
+ def chip_id(self):
+ raise NotSupportedError(self, "chip_id")
+
+ def read_mac(self):
+ """Read MAC from EFUSE region"""
+ words = [self.read_efuse(2), self.read_efuse(1)]
+ bitstring = struct.pack(">II", *words)
+ bitstring = bitstring[2:8] # trim the 2 byte CRC
+ return tuple(bitstring)
+
+ def get_erase_size(self, offset, size):
+ return size
+
+ def override_vddsdio(self, new_voltage):
+ new_voltage = new_voltage.upper()
+ if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES:
+ raise FatalError(
+ "The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'"
+ )
+ RTC_CNTL_SDIO_CONF_REG = 0x3FF48074
+ RTC_CNTL_XPD_SDIO_REG = 1 << 31
+ RTC_CNTL_DREFH_SDIO_M = 3 << 29
+ RTC_CNTL_DREFM_SDIO_M = 3 << 27
+ RTC_CNTL_DREFL_SDIO_M = 3 << 25
+ # RTC_CNTL_SDIO_TIEH = (1 << 23)
+ # not used here, setting TIEH=1 would set 3.3V output,
+ # not safe for esptool.py to do
+ RTC_CNTL_SDIO_FORCE = 1 << 22
+ RTC_CNTL_SDIO_PD_EN = 1 << 21
+
+ reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting
+ reg_val |= RTC_CNTL_SDIO_PD_EN
+ if new_voltage != "OFF":
+ reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO
+ if new_voltage == "1.9V":
+ reg_val |= (
+ RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M
+ ) # boost voltage
+ self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val)
+ print("VDDSDIO regulator set to %s" % new_voltage)
+
+ def read_flash_slow(self, offset, length, progress_fn):
+ BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow)
+
+ data = b""
+ while len(data) < length:
+ block_len = min(BLOCK_LEN, length - len(data))
+ r = self.check_command(
+ "read flash block",
+ self.ESP_READ_FLASH_SLOW,
+ struct.pack("> self.TIMERS_RTC_CALI_VALUE_S
+ ) & self.TIMERS_RTC_CALI_VALUE
+ clk_8M_freq = self.read_efuse(4) & (0xFF) # EFUSE_RD_CK8M_FREQ
+ rom_calculated_freq = cali_val * 15625 * clk_8M_freq / 40
+ return rom_calculated_freq
+
+ def change_baud(self, baud):
+ # It's a workaround to avoid esp32 CK_8M frequency drift.
+ rom_calculated_freq = self.get_rom_cal_crystal_freq()
+ valid_freq = 40000000 if rom_calculated_freq > 33000000 else 26000000
+ false_rom_baud = int(baud * rom_calculated_freq // valid_freq)
+
+ print(f"Changing baud rate to {baud}")
+ self.command(self.ESP_CHANGE_BAUDRATE, struct.pack("> 22) & 0x07
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C2",
+ 1: "ESP32-C2",
+ }.get(self.get_pkg_version(), "unknown ESP32-C2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_minor_chip_version(self):
+ num_word = 1
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF
+
+ def get_major_chip_version(self):
+ num_word = 1
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3
+
+ def get_crystal_freq(self):
+ # The crystal detection algorithm of ESP32/ESP8266 works for ESP32-C2 as well.
+ return ESPLoader.get_crystal_freq(self)
+
+ def change_baud(self, baud):
+ rom_with_26M_XTAL = not self.IS_STUB and self.get_crystal_freq() == 26
+ if rom_with_26M_XTAL:
+ # The code is copied over from ESPLoader.change_baud().
+ # Probably this is just a temporary solution until the next chip revision.
+
+ # The ROM code thinks it uses a 40 MHz XTAL. Recompute the baud rate
+ # in order to trick the ROM code to set the correct baud rate for
+ # a 26 MHz XTAL.
+ false_rom_baud = baud * 40 // 26
+
+ print(f"Changing baud rate to {baud}")
+ self.command(
+ self.ESP_CHANGE_BAUDRATE, struct.pack("> 21) & 0x07
+
+ def get_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C3",
+ }.get(self.get_pkg_version(), "unknown ESP32-C3")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi", "BLE"]
+
+ def get_crystal_freq(self):
+ # ESP32C3 XTAL is fixed to 40MHz
+ return 40
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-C3"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-C3
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+ def uses_usb_jtag_serial(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
+ """
+ if self.secure_download_mode:
+ return False # Can't detect USB-JTAG/Serial in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
+
+ def disable_rtc_watchdog(self):
+ # When USB-JTAG/Serial is used, the RTC watchdog is not reset
+ # and can then reset the board during flashing. Disable it.
+ if self.uses_usb_jtag_serial():
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
+ self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
+
+ def _post_connect(self):
+ if not self.sync_stub_detected: # Don't run if stub is reused
+ self.disable_rtc_watchdog()
+
+
+class ESP32C3StubLoader(ESP32C3ROM):
+ """Access class for ESP32C3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32c6.py b/installer/bin/esptool/esptool/targets/esp32c6.py
new file mode 100644
index 0000000..0c4575e
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32c6.py
@@ -0,0 +1,189 @@
+# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+
+from .esp32c3 import ESP32C3ROM
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32C6ROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-C6"
+ IMAGE_CHIP_ID = 13
+
+ FPGA_SLOW_BOOT = False
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x42800000
+ DROM_MAP_START = 0x42800000
+ DROM_MAP_END = 0x43000000
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ # Magic value for ESP32C6
+ CHIP_DETECT_MAGIC_VALUE = [0x2CE0806F]
+
+ SPI_REG_BASE = 0x60003000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ UART_DATE_REG_ADDR = 0x60000000 + 0x7C
+
+ EFUSE_BASE = 0x600B0800
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ UARTDEV_BUF_NO = 0x4087F580 # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_JTAG_SERIAL = 3 # The above var when USB-JTAG/Serial is used
+
+ DR_REG_LP_WDT_BASE = 0x600B1C00
+ RTC_CNTL_WDTCONFIG0_REG = DR_REG_LP_WDT_BASE + 0x0 # LP_WDT_RWDT_CONFIG0_REG
+ RTC_CNTL_WDTWPROTECT_REG = DR_REG_LP_WDT_BASE + 0x0018 # LP_WDT_RWDT_WPROTECT_REG
+
+ FLASH_FREQUENCY = {
+ "80m": 0x0, # workaround for wrong mspi HS div value in ROM
+ "40m": 0x0,
+ "20m": 0x2,
+ }
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x42800000, 0x43000000, "DROM"],
+ [0x40800000, 0x40880000, "DRAM"],
+ [0x40800000, 0x40880000, "BYTE_ACCESSIBLE"],
+ [0x4004AC00, 0x40050000, "DROM_MASK"],
+ [0x40000000, 0x4004AC00, "IROM_MASK"],
+ [0x42000000, 0x42800000, "IROM"],
+ [0x40800000, 0x40880000, "IRAM"],
+ [0x50000000, 0x50004000, "RTC_IRAM"],
+ [0x50000000, 0x50004000, "RTC_DRAM"],
+ [0x600FE000, 0x60100000, "MEM_INTERNAL2"],
+ ]
+
+ def get_pkg_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
+
+ def get_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C6",
+ }.get(self.get_pkg_version(), "unknown ESP32-C6")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi 6", "BT 5", "IEEE802.15.4"]
+
+ def get_crystal_freq(self):
+ # ESP32C6 XTAL is fixed to 40MHz
+ return 40
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-C6"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-C6
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+
+class ESP32C6StubLoader(ESP32C6ROM):
+ """Access class for ESP32C6 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32C6ROM.STUB_CLASS = ESP32C6StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32c6beta.py b/installer/bin/esptool/esptool/targets/esp32c6beta.py
new file mode 100644
index 0000000..4c4b6be
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32c6beta.py
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32c3 import ESP32C3ROM
+
+
+class ESP32C6BETAROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-C6(beta)"
+ IMAGE_CHIP_ID = 7
+
+ CHIP_DETECT_MAGIC_VALUE = [0x0DA1806F]
+
+ UART_DATE_REG_ADDR = 0x00000500
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-C6",
+ }.get(self.get_pkg_version(), "unknown ESP32-C6")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def _post_connect(self):
+ pass
diff --git a/installer/bin/esptool/esptool/targets/esp32h2.py b/installer/bin/esptool/esptool/targets/esp32h2.py
new file mode 100644
index 0000000..a37617e
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32h2.py
@@ -0,0 +1,65 @@
+# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32c6 import ESP32C6ROM
+
+
+class ESP32H2ROM(ESP32C6ROM):
+ CHIP_NAME = "ESP32-H2"
+ IMAGE_CHIP_ID = 16
+
+ # Magic value for ESP32H2
+ CHIP_DETECT_MAGIC_VALUE = [0xD7B73E80]
+
+ FLASH_FREQUENCY = {
+ "48m": 0xF,
+ "24m": 0x0,
+ "16m": 0x1,
+ "12m": 0x2,
+ }
+
+ def get_pkg_version(self):
+ num_word = 3
+ block1_addr = self.EFUSE_BASE + 0x044
+ word3 = self.read_reg(block1_addr + (4 * num_word))
+ pkg_version = (word3 >> 21) & 0x0F
+ return pkg_version
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-H2",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["BLE"]
+
+ def get_crystal_freq(self):
+ # ESP32H2 XTAL is fixed to 32MHz
+ return 32
+
+
+class ESP32H2StubLoader(ESP32H2ROM):
+ """Access class for ESP32H2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2ROM.STUB_CLASS = ESP32H2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32h2beta1.py b/installer/bin/esptool/esptool/targets/esp32h2beta1.py
new file mode 100644
index 0000000..66b6df9
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32h2beta1.py
@@ -0,0 +1,164 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import struct
+
+from .esp32c3 import ESP32C3ROM
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32H2BETA1ROM(ESP32C3ROM):
+ CHIP_NAME = "ESP32-H2(beta1)"
+ IMAGE_CHIP_ID = 10
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x42800000
+ DROM_MAP_START = 0x3C000000
+ DROM_MAP_END = 0x3C800000
+
+ SPI_REG_BASE = 0x60002000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ CHIP_DETECT_MAGIC_VALUE = [0xCA26CC22]
+
+ UART_DATE_REG_ADDR = 0x60000000 + 0x7C
+
+ EFUSE_BASE = 0x6001A000
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ MEMORY_MAP = []
+
+ FLASH_FREQUENCY = {
+ "48m": 0xF,
+ "24m": 0x0,
+ "16m": 0x1,
+ "12m": 0x2,
+ }
+
+ def get_pkg_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
+
+ def get_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-H2",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["BLE", "IEEE802.15.4"]
+
+ def get_crystal_freq(self):
+ return 32
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-H2"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-H2
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see an AES-128 key
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+ def _post_connect(self):
+ pass
+
+
+class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM):
+ """Access class for ESP32H2BETA1 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32h2beta2.py b/installer/bin/esptool/esptool/targets/esp32h2beta2.py
new file mode 100644
index 0000000..6fa8f58
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32h2beta2.py
@@ -0,0 +1,43 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32h2beta1 import ESP32H2BETA1ROM
+
+
+class ESP32H2BETA2ROM(ESP32H2BETA1ROM):
+ CHIP_NAME = "ESP32-H2(beta2)"
+ IMAGE_CHIP_ID = 14
+
+ CHIP_DETECT_MAGIC_VALUE = [0x6881B06F]
+
+ def get_chip_description(self):
+ chip_name = {
+ 1: "ESP32-H2(beta2)",
+ }.get(self.get_pkg_version(), "unknown ESP32-H2")
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+
+class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM):
+ """Access class for ESP32H2BETA2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32s2.py b/installer/bin/esptool/esptool/targets/esp32s2.py
new file mode 100644
index 0000000..bffd553
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32s2.py
@@ -0,0 +1,305 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+
+from .esp32 import ESP32ROM
+from ..loader import ESPLoader
+from ..reset import HardReset
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32S2ROM(ESP32ROM):
+ CHIP_NAME = "ESP32-S2"
+ IMAGE_CHIP_ID = 2
+
+ FPGA_SLOW_BOOT = False
+
+ IROM_MAP_START = 0x40080000
+ IROM_MAP_END = 0x40B80000
+ DROM_MAP_START = 0x3F000000
+ DROM_MAP_END = 0x3F3F0000
+
+ CHIP_DETECT_MAGIC_VALUE = [0x000007C6]
+
+ SPI_REG_BASE = 0x3F402000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ MAC_EFUSE_REG = 0x3F41A044 # ESP32-S2 has special block for MAC efuses
+
+ UART_CLKDIV_REG = 0x3F400014
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ # todo: use espefuse APIs to get this info
+ EFUSE_BASE = 0x3F41A000
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+ EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x05C
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 19
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
+ EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
+
+ PURPOSE_VAL_XTS_AES256_KEY_1 = 2
+ PURPOSE_VAL_XTS_AES256_KEY_2 = 3
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ UARTDEV_BUF_NO = 0x3FFFFD14 # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_OTG = 2 # Value of the above indicating that USB-OTG is in use
+
+ USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
+
+ GPIO_STRAP_REG = 0x3F404038
+ GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
+ RTC_CNTL_OPTION1_REG = 0x3F408128
+ RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3F000000, 0x3FF80000, "DROM"],
+ [0x3F500000, 0x3FF80000, "EXTRAM_DATA"],
+ [0x3FF9E000, 0x3FFA0000, "RTC_DRAM"],
+ [0x3FF9E000, 0x40000000, "BYTE_ACCESSIBLE"],
+ [0x3FF9E000, 0x40072000, "MEM_INTERNAL"],
+ [0x3FFB0000, 0x40000000, "DRAM"],
+ [0x40000000, 0x4001A100, "IROM_MASK"],
+ [0x40020000, 0x40070000, "IRAM"],
+ [0x40070000, 0x40072000, "RTC_IRAM"],
+ [0x40080000, 0x40800000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ def get_pkg_version(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+ def get_minor_chip_version(self):
+ hi_num_word = 3
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 20) & 0x01
+ low_num_word = 4
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 4) & 0x07
+ return (hi << 3) + low
+
+ def get_major_chip_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x03
+
+ def get_flash_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
+
+ def get_psram_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 28) & 0x0F
+
+ def get_block2_version(self):
+ # BLK_VERSION_MINOR
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 4) & 0x07
+
+ def get_chip_description(self):
+ chip_name = {
+ 0: "ESP32-S2",
+ 1: "ESP32-S2FH2",
+ 2: "ESP32-S2FH4",
+ 102: "ESP32-S2FNR2",
+ 100: "ESP32-S2R2",
+ }.get(
+ self.get_flash_version() + self.get_psram_version() * 100,
+ "unknown ESP32-S2",
+ )
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{chip_name} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+
+ if self.secure_download_mode:
+ features += ["Secure Download Mode Enabled"]
+
+ flash_version = {
+ 0: "No Embedded Flash",
+ 1: "Embedded Flash 2MB",
+ 2: "Embedded Flash 4MB",
+ }.get(self.get_flash_version(), "Unknown Embedded Flash")
+ features += [flash_version]
+
+ psram_version = {
+ 0: "No Embedded PSRAM",
+ 1: "Embedded PSRAM 2MB",
+ 2: "Embedded PSRAM 4MB",
+ }.get(self.get_psram_version(), "Unknown Embedded PSRAM")
+ features += [psram_version]
+
+ block2_version = {
+ 0: "No calibration in BLK2 of efuse",
+ 1: "ADC and temperature sensor calibration in BLK2 of efuse V1",
+ 2: "ADC and temperature sensor calibration in BLK2 of efuse V2",
+ }.get(self.get_block2_version(), "Unknown Calibration in BLK2")
+ features += [block2_version]
+
+ return features
+
+ def get_crystal_freq(self):
+ # ESP32-S2 XTAL is fixed to 40MHz
+ return 40
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-S2"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def flash_type(self):
+ return (
+ 1
+ if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
+ & self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
+ else 0
+ )
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-S2
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see either an AES-128 key or two AES-256 keys
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
+ return True
+
+ return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
+ p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
+ )
+
+ def uses_usb_otg(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect native USB in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
+
+ def _post_connect(self):
+ if self.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+
+ def _check_if_can_reset(self):
+ """
+ Check the strapping register to see if we can reset out of download mode.
+ """
+ if os.getenv("ESPTOOL_TESTING") is not None:
+ print("ESPTOOL_TESTING is set, ignoring strapping mode check")
+ # Esptool tests over USB-OTG run with GPIO0 strapped low,
+ # don't complain in this case.
+ return
+ strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+ force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
+ if (
+ strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
+ and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
+ ):
+ print(
+ "WARNING: {} chip was placed into download mode using GPIO0.\n"
+ "esptool.py can not exit the download mode over USB. "
+ "To run the app, reset the chip manually.\n"
+ "To suppress this note, set --after option to 'no_reset'.".format(
+ self.get_chip_description()
+ )
+ )
+ raise SystemExit(1)
+
+ def hard_reset(self):
+ uses_usb_otg = self.uses_usb_otg()
+ if uses_usb_otg:
+ self._check_if_can_reset()
+
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port, uses_usb_otg)()
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+
+class ESP32S2StubLoader(ESP32S2ROM):
+ """Access class for ESP32-S2 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ if rom_loader.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
+
+
+ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32s3.py b/installer/bin/esptool/esptool/targets/esp32s3.py
new file mode 100644
index 0000000..660537b
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32s3.py
@@ -0,0 +1,315 @@
+# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import struct
+
+from .esp32 import ESP32ROM
+from ..loader import ESPLoader
+from ..reset import HardReset
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP32S3ROM(ESP32ROM):
+ CHIP_NAME = "ESP32-S3"
+
+ IMAGE_CHIP_ID = 9
+
+ CHIP_DETECT_MAGIC_VALUE = [0x9]
+
+ FPGA_SLOW_BOOT = False
+
+ IROM_MAP_START = 0x42000000
+ IROM_MAP_END = 0x44000000
+ DROM_MAP_START = 0x3C000000
+ DROM_MAP_END = 0x3E000000
+
+ UART_DATE_REG_ADDR = 0x60000080
+
+ SPI_REG_BASE = 0x60002000
+ SPI_USR_OFFS = 0x18
+ SPI_USR1_OFFS = 0x1C
+ SPI_USR2_OFFS = 0x20
+ SPI_MOSI_DLEN_OFFS = 0x24
+ SPI_MISO_DLEN_OFFS = 0x28
+ SPI_W0_OFFS = 0x58
+
+ BOOTLOADER_FLASH_OFFSET = 0x0
+
+ SUPPORTS_ENCRYPTED_FLASH = True
+
+ FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+ # todo: use espefuse APIs to get this info
+ EFUSE_BASE = 0x60007000 # BLOCK0 read base address
+ EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x44
+ EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x5C
+ MAC_EFUSE_REG = EFUSE_BASE + 0x044
+
+ EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
+
+ EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY0_SHIFT = 24
+ EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+ EFUSE_PURPOSE_KEY1_SHIFT = 28
+ EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY2_SHIFT = 0
+ EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY3_SHIFT = 4
+ EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY4_SHIFT = 8
+ EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+ EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+ EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+ EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
+ EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
+
+ EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
+ EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
+
+ EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
+ EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
+
+ PURPOSE_VAL_XTS_AES256_KEY_1 = 2
+ PURPOSE_VAL_XTS_AES256_KEY_2 = 3
+ PURPOSE_VAL_XTS_AES128_KEY = 4
+
+ UARTDEV_BUF_NO = 0x3FCEF14C # Variable in ROM .bss which indicates the port in use
+ UARTDEV_BUF_NO_USB_OTG = 3 # The above var when USB-OTG is used
+ UARTDEV_BUF_NO_USB_JTAG_SERIAL = 4 # The above var when USB-JTAG/Serial is used
+
+ RTC_CNTL_WDT_WKEY = 0x50D83AA1
+ RTCCNTL_BASE_REG = 0x60008000
+ RTC_CNTL_WDTCONFIG0_REG = RTCCNTL_BASE_REG + 0x0090
+ RTC_CNTL_WDTWPROTECT_REG = RTCCNTL_BASE_REG + 0x00B0
+
+ USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
+
+ GPIO_STRAP_REG = 0x60004038
+ GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
+ RTC_CNTL_OPTION1_REG = 0x6000812C
+ RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
+
+ UART_CLKDIV_REG = 0x60000014
+
+ MEMORY_MAP = [
+ [0x00000000, 0x00010000, "PADDING"],
+ [0x3C000000, 0x3D000000, "DROM"],
+ [0x3D000000, 0x3E000000, "EXTRAM_DATA"],
+ [0x600FE000, 0x60100000, "RTC_DRAM"],
+ [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
+ [0x3FC88000, 0x403E2000, "MEM_INTERNAL"],
+ [0x3FC88000, 0x3FD00000, "DRAM"],
+ [0x40000000, 0x4001A100, "IROM_MASK"],
+ [0x40370000, 0x403E0000, "IRAM"],
+ [0x600FE000, 0x60100000, "RTC_IRAM"],
+ [0x42000000, 0x42800000, "IROM"],
+ [0x50000000, 0x50002000, "RTC_DATA"],
+ ]
+
+ def get_pkg_version(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
+
+ def is_eco0(self, minor_raw):
+ # Workaround: The major version field was allocated to other purposes
+ # when block version is v1.1.
+ # Luckily only chip v0.0 have this kind of block version and efuse usage.
+ return (
+ (minor_raw & 0x7) == 0
+ and self.get_blk_version_major() == 1
+ and self.get_blk_version_minor() == 1
+ )
+
+ def get_minor_chip_version(self):
+ minor_raw = self.get_raw_minor_chip_version()
+ if self.is_eco0(minor_raw):
+ return 0
+ return minor_raw
+
+ def get_raw_minor_chip_version(self):
+ hi_num_word = 5
+ hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+ low_num_word = 3
+ low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+ return (hi << 3) + low
+
+ def get_blk_version_major(self):
+ num_word = 4
+ return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 0) & 0x03
+
+ def get_blk_version_minor(self):
+ num_word = 3
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07
+
+ def get_major_chip_version(self):
+ minor_raw = self.get_raw_minor_chip_version()
+ if self.is_eco0(minor_raw):
+ return 0
+ return self.get_raw_major_chip_version()
+
+ def get_raw_major_chip_version(self):
+ num_word = 5
+ return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
+ def get_chip_description(self):
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{self.CHIP_NAME} (revision v{major_rev}.{minor_rev})"
+
+ def get_chip_features(self):
+ return ["WiFi", "BLE"]
+
+ def get_crystal_freq(self):
+ # ESP32S3 XTAL is fixed to 40MHz
+ return 40
+
+ def get_flash_crypt_config(self):
+ return None # doesn't exist on ESP32-S3
+
+ def get_key_block_purpose(self, key_block):
+ if key_block < 0 or key_block > 5:
+ raise FatalError("Valid key block numbers must be in range 0-5")
+
+ reg, shift = [
+ (self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+ (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+ (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+ (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+ (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+ (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
+ ][key_block]
+ return (self.read_reg(reg) >> shift) & 0xF
+
+ def is_flash_encryption_key_valid(self):
+ # Need to see either an AES-128 key or two AES-256 keys
+ purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+ if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
+ return True
+
+ return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
+ p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
+ )
+
+ def get_secure_boot_enabled(self):
+ return (
+ self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
+ & self.EFUSE_SECURE_BOOT_EN_MASK
+ )
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "VDD_SDIO overrides are not supported for ESP32-S3"
+ )
+
+ def read_mac(self):
+ mac0 = self.read_reg(self.MAC_EFUSE_REG)
+ mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
+ bitstring = struct.pack(">II", mac1, mac0)[2:]
+ return tuple(bitstring)
+
+ def flash_type(self):
+ return (
+ 1
+ if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
+ & self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
+ else 0
+ )
+
+ def uses_usb_otg(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect native USB in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
+
+ def uses_usb_jtag_serial(self):
+ """
+ Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
+ """
+ if self.secure_download_mode:
+ return False # can't detect USB-JTAG/Serial in secure download mode
+ return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
+
+ def disable_rtc_watchdog(self):
+ # When USB-JTAG/Serial is used, the RTC watchdog is not reset
+ # and can then reset the board during flashing. Disable it.
+ if self.uses_usb_jtag_serial():
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
+ self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
+ self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
+
+ def _post_connect(self):
+ if self.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ if not self.sync_stub_detected: # Don't run if stub is reused
+ self.disable_rtc_watchdog()
+
+ def _check_if_can_reset(self):
+ """
+ Check the strapping register to see if we can reset out of download mode.
+ """
+ if os.getenv("ESPTOOL_TESTING") is not None:
+ print("ESPTOOL_TESTING is set, ignoring strapping mode check")
+ # Esptool tests over USB-OTG run with GPIO0 strapped low,
+ # don't complain in this case.
+ return
+ strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+ force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
+ if (
+ strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
+ and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
+ ):
+ print(
+ "WARNING: {} chip was placed into download mode using GPIO0.\n"
+ "esptool.py can not exit the download mode over USB. "
+ "To run the app, reset the chip manually.\n"
+ "To suppress this note, set --after option to 'no_reset'.".format(
+ self.get_chip_description()
+ )
+ )
+ raise SystemExit(1)
+
+ def hard_reset(self):
+ uses_usb_otg = self.uses_usb_otg()
+ if uses_usb_otg:
+ self._check_if_can_reset()
+
+ print("Hard resetting via RTS pin...")
+ HardReset(self._port, uses_usb_otg)()
+
+ def change_baud(self, baud):
+ ESPLoader.change_baud(self, baud)
+
+
+class ESP32S3StubLoader(ESP32S3ROM):
+ """Access class for ESP32S3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ if rom_loader.uses_usb_otg():
+ self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+ self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
+
+
+ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp32s3beta2.py b/installer/bin/esptool/esptool/targets/esp32s3beta2.py
new file mode 100644
index 0000000..b7958bd
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp32s3beta2.py
@@ -0,0 +1,42 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from .esp32s3 import ESP32S3ROM
+
+
+class ESP32S3BETA2ROM(ESP32S3ROM):
+ CHIP_NAME = "ESP32-S3(beta2)"
+ IMAGE_CHIP_ID = 4
+
+ CHIP_DETECT_MAGIC_VALUE = [0xEB004136]
+
+ EFUSE_BASE = 0x6001A000 # BLOCK0 read base address
+
+ def get_chip_description(self):
+ major_rev = self.get_major_chip_version()
+ minor_rev = self.get_minor_chip_version()
+ return f"{self.CHIP_NAME} (revision v{major_rev}.{minor_rev})"
+
+
+class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM):
+ """Access class for ESP32S3 stub loader, runs on top of ROM.
+
+ (Basically the same as ESP32StubLoader, but different base class.
+ Can possibly be made into a mixin.)
+ """
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+
+ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader
diff --git a/installer/bin/esptool/esptool/targets/esp8266.py b/installer/bin/esptool/esptool/targets/esp8266.py
new file mode 100644
index 0000000..f996663
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/esp8266.py
@@ -0,0 +1,191 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from ..loader import ESPLoader
+from ..util import FatalError, NotImplementedInROMError
+
+
+class ESP8266ROM(ESPLoader):
+ """Access class for ESP8266 ROM bootloader"""
+
+ CHIP_NAME = "ESP8266"
+ IS_STUB = False
+
+ CHIP_DETECT_MAGIC_VALUE = [0xFFF0C101]
+
+ # OTP ROM addresses
+ ESP_OTP_MAC0 = 0x3FF00050
+ ESP_OTP_MAC1 = 0x3FF00054
+ ESP_OTP_MAC3 = 0x3FF0005C
+
+ SPI_REG_BASE = 0x60000200
+ SPI_USR_OFFS = 0x1C
+ SPI_USR1_OFFS = 0x20
+ SPI_USR2_OFFS = 0x24
+ SPI_MOSI_DLEN_OFFS = None
+ SPI_MISO_DLEN_OFFS = None
+ SPI_W0_OFFS = 0x40
+
+ UART_CLKDIV_REG = 0x60000014
+
+ XTAL_CLK_DIVIDER = 2
+
+ FLASH_SIZES = {
+ "512KB": 0x00,
+ "256KB": 0x10,
+ "1MB": 0x20,
+ "2MB": 0x30,
+ "4MB": 0x40,
+ "2MB-c1": 0x50,
+ "4MB-c1": 0x60,
+ "8MB": 0x80,
+ "16MB": 0x90,
+ }
+
+ FLASH_FREQUENCY = {
+ "80m": 0xF,
+ "40m": 0x0,
+ "26m": 0x1,
+ "20m": 0x2,
+ }
+
+ BOOTLOADER_FLASH_OFFSET = 0
+
+ MEMORY_MAP = [
+ [0x3FF00000, 0x3FF00010, "DPORT"],
+ [0x3FFE8000, 0x40000000, "DRAM"],
+ [0x40100000, 0x40108000, "IRAM"],
+ [0x40201010, 0x402E1010, "IROM"],
+ ]
+
+ def get_efuses(self):
+ # Return the 128 bits of ESP8266 efuse as a single Python integer
+ result = self.read_reg(0x3FF0005C) << 96
+ result |= self.read_reg(0x3FF00058) << 64
+ result |= self.read_reg(0x3FF00054) << 32
+ result |= self.read_reg(0x3FF00050)
+ return result
+
+ def _get_flash_size(self, efuses):
+ # rX_Y = EFUSE_DATA_OUTX[Y]
+ r0_4 = (efuses & (1 << 4)) != 0
+ r3_25 = (efuses & (1 << 121)) != 0
+ r3_26 = (efuses & (1 << 122)) != 0
+ r3_27 = (efuses & (1 << 123)) != 0
+
+ if r0_4 and not r3_25:
+ if not r3_27 and not r3_26:
+ return 1
+ elif not r3_27 and r3_26:
+ return 2
+ if not r0_4 and r3_25:
+ if not r3_27 and not r3_26:
+ return 2
+ elif not r3_27 and r3_26:
+ return 4
+ return -1
+
+ def get_chip_description(self):
+ efuses = self.get_efuses()
+ is_8285 = (
+ efuses & ((1 << 4) | 1 << 80)
+ ) != 0 # One or the other efuse bit is set for ESP8285
+ if is_8285:
+ flash_size = self._get_flash_size(efuses)
+ max_temp = (
+ efuses & (1 << 5)
+ ) != 0 # This efuse bit identifies the max flash temperature
+ chip_name = {
+ 1: "ESP8285H08" if max_temp else "ESP8285N08",
+ 2: "ESP8285H16" if max_temp else "ESP8285N16",
+ }.get(flash_size, "ESP8285")
+ return chip_name
+ return "ESP8266EX"
+
+ def get_chip_features(self):
+ features = ["WiFi"]
+ if "ESP8285" in self.get_chip_description():
+ features += ["Embedded Flash"]
+ return features
+
+ def flash_spi_attach(self, hspi_arg):
+ if self.IS_STUB:
+ super(ESP8266ROM, self).flash_spi_attach(hspi_arg)
+ else:
+ # ESP8266 ROM has no flash_spi_attach command in serial protocol,
+ # but flash_begin will do it
+ self.flash_begin(0, 0)
+
+ def flash_set_parameters(self, size):
+ # not implemented in ROM, but OK to silently skip for ROM
+ if self.IS_STUB:
+ super(ESP8266ROM, self).flash_set_parameters(size)
+
+ def chip_id(self):
+ """
+ Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() func
+ """
+ id0 = self.read_reg(self.ESP_OTP_MAC0)
+ id1 = self.read_reg(self.ESP_OTP_MAC1)
+ return (id0 >> 24) | ((id1 & 0xFFFFFF) << 8)
+
+ def read_mac(self):
+ """Read MAC from OTP ROM"""
+ mac0 = self.read_reg(self.ESP_OTP_MAC0)
+ mac1 = self.read_reg(self.ESP_OTP_MAC1)
+ mac3 = self.read_reg(self.ESP_OTP_MAC3)
+ if mac3 != 0:
+ oui = ((mac3 >> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF)
+ elif ((mac1 >> 16) & 0xFF) == 0:
+ oui = (0x18, 0xFE, 0x34)
+ elif ((mac1 >> 16) & 0xFF) == 1:
+ oui = (0xAC, 0xD0, 0x74)
+ else:
+ raise FatalError("Unknown OUI")
+ return oui + ((mac1 >> 8) & 0xFF, mac1 & 0xFF, (mac0 >> 24) & 0xFF)
+
+ def get_erase_size(self, offset, size):
+ """Calculate an erase size given a specific size in bytes.
+
+ Provides a workaround for the bootloader erase bug."""
+
+ sectors_per_block = 16
+ sector_size = self.FLASH_SECTOR_SIZE
+ num_sectors = (size + sector_size - 1) // sector_size
+ start_sector = offset // sector_size
+
+ head_sectors = sectors_per_block - (start_sector % sectors_per_block)
+ if num_sectors < head_sectors:
+ head_sectors = num_sectors
+
+ if num_sectors < 2 * head_sectors:
+ return (num_sectors + 1) // 2 * sector_size
+ else:
+ return (num_sectors - head_sectors) * sector_size
+
+ def override_vddsdio(self, new_voltage):
+ raise NotImplementedInROMError(
+ "Overriding VDDSDIO setting only applies to ESP32"
+ )
+
+
+class ESP8266StubLoader(ESP8266ROM):
+ """Access class for ESP8266 stub loader, runs on top of ROM."""
+
+ FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
+ IS_STUB = True
+
+ def __init__(self, rom_loader):
+ self.secure_download_mode = rom_loader.secure_download_mode
+ self._port = rom_loader._port
+ self._trace_enabled = rom_loader._trace_enabled
+ self.cache = rom_loader.cache
+ self.flush_input() # resets _slip_reader
+
+ def get_erase_size(self, offset, size):
+ return size # stub doesn't have same size bug as ROM loader
+
+
+ESP8266ROM.STUB_CLASS = ESP8266StubLoader
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32.json
new file mode 100644
index 0000000..3e87d81
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1074521560,
+ "text": "CAD0PxwA9D8AAPQ/AMD8PxAA9D82QQAh+v/AIAA4AkH5/8AgACgEICB0nOIGBQAAAEH1/4H2/8AgAKgEiAigoHTgCAALImYC54b0/yHx/8AgADkCHfAAAKDr/T8Ya/0/hIAAAEBAAABYq/0/pOv9PzZBALH5/yCgdBARIKXHAJYaBoH2/5KhAZCZEZqYwCAAuAmR8/+goHSaiMAgAJIYAJCQ9BvJwMD0wCAAwlgAmpvAIACiSQDAIACSGACB6v+QkPSAgPSHmUeB5f+SoQGQmRGamMAgAMgJoeX/seP/h5wXxgEAfOiHGt7GCADAIACJCsAgALkJRgIAwCAAuQrAIACJCZHX/5qIDAnAIACSWAAd8AAA+CD0P/gw9D82QQCR/f/AIACICYCAJFZI/5H6/8AgAIgJgIAkVkj/HfAAAAAQIPQ/ACD0PwAAAAg2QQAQESCl/P8h+v8MCMAgAIJiAJH6/4H4/8AgAJJoAMAgAJgIVnn/wCAAiAJ88oAiMCAgBB3wAAAAAEA2QQAQESDl+/8Wav+B7P+R+//AIACSaADAIACYCFZ5/x3wAAAMwPw/////AAQg9D82QQAh/P84QhaDBhARIGX4/xb6BQz4DAQ3qA2YIoCZEIKgAZBIg0BAdBARICX6/xARICXz/4giDBtAmBGQqwHMFICrAbHt/7CZELHs/8AgAJJrAJHO/8AgAKJpAMAgAKgJVnr/HAkMGkCag5AzwJqIOUKJIh3wAAAskgBANkEAoqDAgf3/4AgAHfAAADZBAIKgwK0Ch5IRoqDbgff/4AgAoqDcRgQAAAAAgqDbh5IIgfL/4AgAoqDdgfD/4AgAHfA2QQA6MsYCAACiAgAbIhARIKX7/zeS8R3wAAAAfNoFQNguBkCc2gVAHNsFQDYhIaLREIH6/+AIAEYLAAAADBRARBFAQ2PNBL0BrQKB9f/gCACgoHT8Ws0EELEgotEQgfH/4AgASiJAM8BWA/0iogsQIrAgoiCy0RCB7P/gCACtAhwLEBEgpff/LQOGAAAioGMd8AAA/GcAQNCSAEAIaABANkEhYqEHwGYRGmZZBiwKYtEQDAVSZhqB9//gCAAMGECIEUe4AkZFAK0GgdT/4AgAhjQAAJKkHVBzwOCZERqZQHdjiQnNB70BIKIggc3/4AgAkqQd4JkRGpmgoHSICYyqDAiCZhZ9CIYWAAAAkqQd4JkREJmAgmkAEBEgJer/vQetARARIKXt/xARICXp/80HELEgYKYggbv/4AgAkqQd4JkRGpmICXAigHBVgDe1sJKhB8CZERqZmAmAdcCXtwJG3P+G5v8MCIJGbKKkGxCqoIHK/+AIAFYK/7KiC6IGbBC7sBARIKWPAPfqEvZHD7KiDRC7sHq7oksAG3eG8f9867eawWZHCIImGje4Aoe1nCKiCxAisGC2IK0CgZv/4AgAEBEgpd//rQIcCxARICXj/xARIKXe/ywKgbH/4AgAHfAIIPQ/cOL6P0gkBkDwIgZANmEAEBEg5cr/EKEggfv/4AgAPQoMEvwqiAGSogCQiBCJARARIKXP/5Hy/6CiAcAgAIIpAKCIIMAgAIJpALIhAKHt/4Hu/+AIAKAjgx3wAAD/DwAANkEAgTv/DBmSSAAwnEGZKJH7/zkYKTgwMLSaIiozMDxBDAIpWDlIEBEgJfj/LQqMGiKgxR3wAABQLQZANkEAQSz/WDRQM2MWYwRYFFpTUFxBRgEAEBEgZcr/iESmGASIJIel7xARIKXC/xZq/6gUzQO9AoHx/+AIAKCgdIxKUqDEUmQFWBQ6VVkUWDQwVcBZNB3wAADA/D9PSEFJqOv9P3DgC0AU4AtADAD0PzhA9D///wAAjIAAABBAAACs6/0/vOv9PwTA/D8IwPw/BOz9PxQA9D/w//8AqOv9Pxjr/D8kwPw/fGgAQOxnAEBYhgBAbCoGQDgyBkAULAZAzCwGQEwsBkA0hQBAzJAAQHguBkAw7wVAWJIAQEyCAEA2wQAh3v8MCiJhCEKgAIHu/+AIACHZ/zHa/8YAAEkCSyI3MvgQESBlw/8MS6LBIBARIOXG/yKhARARICXC/1GR/pAiESolMc//sc//wCAAWQIheP4MDAxaMmIAgdz/4AgAMcr/QqEBwCAAKAMsCkAiIMAgACkDgTH/4AgAgdX/4AgAIcP/wCAAKALMuhzDMCIQIsL4DBMgo4MMC4HO/+AIAPG8/wwdwqABDBvioQBA3REAzBGAuwGioACBx//gCAAhtv8MBCpVIcP+ctIrwCAAKAUWcv/AIAA4BQwSwCAASQUiQRAiAwEMKCJBEYJRCUlRJpIHHDiHEh4GCAAiAwOCAwKAIhGAIiBmQhEoI8AgACgCKVFGAQAAHCIiUQkQESCls/8Mi6LBEBARIGW3/4IDAyIDAoCIESCIICGY/yAg9IeyHKKgwBARICWy/6Kg7hARIKWx/xARICWw/4bb/wAAIgMBHDknOTT2IhjG1AAAACLCLyAgdPZCcJGJ/5AioCgCoAIAIsL+ICB0HBknuQLGywCRhP+QIqAoAqACAJLCMJCQdLZZyQbGACxKbQQioMCnGAIGxABJUQxyrQQQESDlqv+tBBARIGWq/xARIOWo/xARIKWo/wyLosEQIsL/EBEg5av/ViL9RikADBJWyCyCYQ+Bev/gCACI8aAog8auACaIBAwSxqwAmCNoM2CJIICAtFbY/pnBEBEgZcf/mMFqKZwqBvf/AACgrEGBbf/gCABW6vxi1vBgosDMJgaBAACgkPRWGf6GBACgoPWZwYFl/+AIAJjBVpr6kGbADBkAmRFgosBnOeEGBAAAAKCsQYFc/+AIAFaq+GLW8GCiwFam/sZvAABtBCKgwCaIAoaNAG0EDALGiwAAACa484ZhAAwSJrgCBoUAuDOoIxARIOWh/6AkgwaBAAwcZrhTiEMgrBFtBCKgwoe6AoZ+ALhTqCPJ4RARIOXA/8YLAAwcZrgviEMgrBFtBCKgwoe6AoZ1ACgzuFOoIyBogsnhEBEgZb7/ITT+SWIi0itpIsjhoMSDLQyGaQChL/5tBLIKACKgxhY7GpgjgsjwIqDAh5kBKFoMCaKg70YCAJqzsgsYG5mwqjCHKfKCAwWSAwSAiBGQiCCSAwZtBACZEYCZIIIDB4CIAZCIIICqwIKgwaAok0ZVAIEY/m0EoggAIqDGFnoUqDgioMhW+hMoWKJIAMZNAByKbQQMEqcYAsZKAPhz6GPYU8hDuDOoI4EM/+AIAG0KoCSDRkQAAAwSJkgCRj8AqCO9BIEE/+AIAAYeAICwNG0EIqDAVgsPgGRBi8N8/UYOAKg8ucHJ4dnRgQD/4AgAyOG4wSgsmByoDNIhDZCSECYCDsAgAOIqACAtMOAiECCZIMAgAJkKG7vCzBBnO8LGm/9mSAJGmv9tBCKgwAYmAAwSJrgCRiEAIdz+mFOII5kCIdv+iQItBIYcAGHX/gwb2AaCyPCtBC0EgCuT0KuDIKoQbQQioMZW6gXB0f4ioMnoDIc+U4DwFCKgwFavBC0KRgIAKqOoaksiqQmtCyD+wCqdhzLtFprfIcT++QyZAsZ7/wwSZogWIcH+iAIWKACCoMhJAiG9/kkCDBKAJINtBEYBAABtBCKg/yCgdBARIOV5/2CgdBARIGV5/xARIOV3/1aiviIDARwoJzge9jICBvf+IsL9ICB0DPgnuAKG8/6BrP6AIqAoAqACAIKg0ocSUoKg1IcSegbt/gAAAIgzoqJxwKoRaCOJ8YGw/uAIACGh/pGi/sAgACgCiPEgNDXAIhGQIhAgIyCAIoKtBGCywoGn/uAIAKKj6IGk/uAIAAbb/gAA2FPIQ7gzqCMQESAlff9G1v4AsgMDIgMCgLsRILsgssvwosMYEBEgZZn/Rs/+ACIDA4IDAmGP/YAiEZg2gCIgIsLwkCJjFiKymBaakpCcQUYCAJnBEBEgZWL/mMGoRqYaBKgmp6nrEBEgpVr/Fmr/qBbNArLDGIGG/uAIAIw6MqDEOVY4FiozORY4NiAjwCk2xrX+ggMCIsMYMgMDDByAMxGAMyAyw/AGIwCBbP6RHf3oCDlx4JnAmWGYJwwal7MBDDqJ8anR6cEQESAlW/+o0ZFj/ujBqQGhYv7dCb0CwsEc8sEYmcGBa/7gCAC4J80KqHGI8aC7wLknoDPAuAiqIqhhmMGqu90EDBq5CMDag5C7wNDgdMx90tuA0K6TFmoBrQmJ8ZnByeEQESAlif+I8ZjByOGSaABhTv2INoyjwJ8xwJnA1ikAVvj11qwAMUn9IqDHKVNGAACMPJwIxoL+FoigYUT9IqDIKVZGf/4AMUH9IqDJKVNGfP4oI1bCnq0EgUX+4AgAoqJxwKoRgT7+4AgAgUL+4AgAxnP+AAAoMxaCnK0EgTz+4AgAoqPogTb+4AgA4AIARmz+HfAAAAA2QQCdAoKgwCgDh5kPzDIMEoYHAAwCKQN84oYPACYSByYiGIYDAAAAgqDbgCkjh5kqDCIpA3zyRggAAAAioNwnmQoMEikDLQgGBAAAAIKg3Xzyh5kGDBIpAyKg2x3wAAA=",
+ "text_start": 1074520064,
+ "data": "GOv8P9jnC0Bx6AtA8+wLQO3oC0CP6AtA7egLQEnpC0AG6gtAeOoLQCHqC0CB5wtAo+kLQPjpC0Bn6QtAmuoLQI7pC0Ca6gtAXegLQLPoC0Dt6AtASekLQHfoC0BM6wtAs+wLQKXmC0DX7AtApeYLQKXmC0Cl5gtApeYLQKXmC0Cl5gtApeYLQKXmC0Dz6gtApeYLQM3rC0Cz7AtA",
+ "data_start": 1073605544
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c2.json
new file mode 100644
index 0000000..07887d4
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413304,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvKP+AIOEAsCThAhAk4QCgKOECUCjhAQgo4QKgHOEDkCThAJAo4QJgJOEBYBzhAzAk4QFgHOEC6CDhA/gg4QCwJOECECThAzAg4QBIIOEBCCDhAyAg4QOYMOEAsCThArAs4QJoMOECkBjhAxAw4QKQGOECkBjhApAY4QKQGOECkBjhApAY4QKQGOECkBjhASAs4QKQGOEDICzhAmgw4QA==",
+ "data_start": 1070295976
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c3.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c3.json
new file mode 100644
index 0000000..8426a63
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c3.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413532,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "IGvIP1YKOECmCjhA/go4QKILOEAODDhAvAs4QCIJOEBeCzhAngs4QBILOEDSCDhARgs4QNIIOEAwCjhAdgo4QKYKOED+CjhAQgo4QIYJOEC2CThAPgo4QGAOOECmCjhAJg04QBgOOEASCDhAQA44QBIIOEASCDhAEgg4QBIIOEASCDhAEgg4QBIIOEASCDhAwgw4QBIIOEBEDThAGA44QA==",
+ "data_start": 1070164912
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6.json
new file mode 100644
index 0000000..2bf3acc
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1082132112,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "HCuEQCoKgEB6CoBA0gqAQHYLgEDiC4BAkAuAQPYIgEAyC4BAcguAQOYKgECmCIBAGguAQKYIgEAECoBASgqAQHoKgEDSCoBAFgqAQFoJgECKCYBAEgqAQDQOgEB6CoBA+gyAQOwNgEDmB4BAFA6AQOYHgEDmB4BA5geAQOYHgEDmB4BA5geAQOYHgEDmB4BAlgyAQOYHgEAYDYBA7A2AQA==",
+ "data_start": 1082469292
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6beta.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6beta.json
new file mode 100644
index 0000000..4709f9d
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32c6beta.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
+ "data_start": 1070164904
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2.json
new file mode 100644
index 0000000..f003a55
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1082132112,
+ "text": "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",
+ "text_start": 1082130432,
+ "data": "HCuDQCoKgEB6CoBA0gqAQHYLgEDiC4BAkAuAQPYIgEAyC4BAcguAQOYKgECmCIBAGguAQKYIgEAECoBASgqAQHoKgEDSCoBAFgqAQFoJgECKCYBAEgqAQDQOgEB6CoBA+gyAQOwNgEDmB4BAFA6AQOYHgEDmB4BA5geAQOYHgEDmB4BA5geAQOYHgEDmB4BAlgyAQOYHgEAYDYBA7A2AQA==",
+ "data_start": 1082403756
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta1.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta1.json
new file mode 100644
index 0000000..37d29e8
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta1.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
+ "data_start": 1070164904
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta2.json
new file mode 100644
index 0000000..f7cc1ce
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32h2beta2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077413318,
+ "text": "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",
+ "text_start": 1077411840,
+ "data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
+ "data_start": 1070164904
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s2.json
new file mode 100644
index 0000000..de8cdc2
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1073907696,
+ "text": "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",
+ "text_start": 1073905664,
+ "data": "ZCv9PzaLAkDBiwJAhpACQEqMAkDjiwJASowCQKmMAkByjQJA5Y0CQI2NAkDAigJAC40CQGSNAkDMjAJACI4CQPaMAkAIjgJAr4sCQA6MAkBKjAJAqYwCQMeLAkACiwJAx44CQD2QAkDYiQJAZZACQNiJAkDYiQJA2IkCQNiJAkDYiQJA2IkCQNiJAkDYiQJAZI4CQNiJAkBZjwJAPZACQA==",
+ "data_start": 1073622012
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3.json
new file mode 100644
index 0000000..1a73fca
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077381684,
+ "text": "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",
+ "text_start": 1077379072,
+ "data": "ZCvKP5aNN0B7jjdAPJM3QAaPN0CbjjdABo83QGWPN0AykDdApZA3QE2QN0AhjTdAyI83QCSQN0CIjzdAx5A3QLKPN0DHkDdAaY43QMaON0AGjzdAZY83QIGON0BijTdAiJE3QAKTN0A+jDdAIpM3QD6MN0A+jDdAPow3QD6MN0A+jDdAPow3QD6MN0A+jDdAIpE3QD6MN0AdkjdAApM3QAQInwAAAAAAAAAYAQQIBQAAAAAAAAAIAQQIBgAAAAAAAAAAAQQIIQAAAAAAIAAAEQQI3AAAAAAAIAAAEQQIDAAAAAAAIAAAAQQIEgAAAAAAIAAAESAoDAAQAQAA",
+ "data_start": 1070279668
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3beta2.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3beta2.json
new file mode 100644
index 0000000..8621a93
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_32s3beta2.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1077381176,
+ "text": "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",
+ "text_start": 1077379072,
+ "data": "ZCvKP4KLN0APjDdA15A3QJqMN0AvjDdAmow3QPmMN0DGjTdAOY43QOGNN0ANizdAXI03QLiNN0AcjTdAXI43QEaNN0BcjjdA/Ys3QFqMN0CajDdA+Yw3QBWMN0BOizdAHI83QJuQN0AsijdAvZA3QCyKN0AsijdALIo3QCyKN0AsijdALIo3QCyKN0AsijdAto43QCyKN0CyjzdAm5A3QA==",
+ "data_start": 1070279668
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_8266.json b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_8266.json
new file mode 100644
index 0000000..8d42b24
--- /dev/null
+++ b/installer/bin/esptool/esptool/targets/stub_flasher/stub_flasher_8266.json
@@ -0,0 +1,7 @@
+{
+ "entry": 1074843652,
+ "text": "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",
+ "text_start": 1074843648,
+ "data": "CIH+PwUFBAACAwcAAwMLALnXEEDv1xBAHdgQQLrYEEBo5xBAHtkQQHTZEEDA2RBAaOcQQILaEED/2hBAwNsQQGjnEEBo5xBAWNwQQGjnEEA33xBAAOAQQDvgEEBo5xBAaOcQQNfgEEBo5xBAv+EQQGXiEECj4xBAY+QQQDTlEEBo5xBAaOcQQGjnEEBo5xBAYuYQQGjnEEBX5xBAkN0QQI/YEECm5RBAq9oQQPzZEEBo5xBA7OYQQDHnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQCLaEEBf2hBAvuUQQAEAAAACAAAAAwAAAAQAAAAFAAAABwAAAAkAAAANAAAAEQAAABkAAAAhAAAAMQAAAEEAAABhAAAAgQAAAMEAAAABAQAAgQEAAAECAAABAwAAAQQAAAEGAAABCAAAAQwAAAEQAAABGAAAASAAAAEwAAABQAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAAAAAAAAAAAAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAANAAAADwAAABEAAAATAAAAFwAAABsAAAAfAAAAIwAAACsAAAAzAAAAOwAAAEMAAABTAAAAYwAAAHMAAACDAAAAowAAAMMAAADjAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAAEAAAABAAAAAQAAAAEAAAABQAAAAUAAAAFAAAABQAAAAAAAAAAAAAAAAAAABAREgAIBwkGCgULBAwDDQIOAQ8AAQEAAAEAAAAEAAAA",
+ "data_start": 1073720488
+}
\ No newline at end of file
diff --git a/installer/bin/esptool/esptool/util.py b/installer/bin/esptool/esptool/util.py
new file mode 100644
index 0000000..66d9bd4
--- /dev/null
+++ b/installer/bin/esptool/esptool/util.py
@@ -0,0 +1,174 @@
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
+# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import re
+import struct
+import sys
+
+
+def byte(bitstr, index):
+ return bitstr[index]
+
+
+def mask_to_shift(mask):
+ """Return the index of the least significant bit in the mask"""
+ shift = 0
+ while mask & 0x1 == 0:
+ shift += 1
+ mask >>= 1
+ return shift
+
+
+def div_roundup(a, b):
+ """Return a/b rounded up to nearest integer,
+ equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only
+ without possible floating point accuracy errors.
+ """
+ return (int(a) + int(b) - 1) // int(b)
+
+
+def flash_size_bytes(size):
+ """Given a flash size of the type passed in args.flash_size
+ (ie 512KB or 1MB) then return the size in bytes.
+ """
+ if "MB" in size:
+ return int(size[: size.index("MB")]) * 1024 * 1024
+ elif "KB" in size:
+ return int(size[: size.index("KB")]) * 1024
+ else:
+ raise FatalError("Unknown size %s" % size)
+
+
+def hexify(s, uppercase=True):
+ format_str = "%02X" if uppercase else "%02x"
+ return "".join(format_str % c for c in s)
+
+
+def pad_to(data, alignment, pad_character=b"\xFF"):
+ """Pad to the next alignment boundary"""
+ pad_mod = len(data) % alignment
+ if pad_mod != 0:
+ data += pad_character * (alignment - pad_mod)
+ return data
+
+
+def print_overwrite(message, last_line=False):
+ """Print a message, overwriting the currently printed line.
+
+ If last_line is False, don't append a newline at the end
+ (expecting another subsequent call will overwrite this one.)
+
+ After a sequence of calls with last_line=False, call once with last_line=True.
+
+ If output is not a TTY (for example redirected a pipe),
+ no overwriting happens and this function is the same as print().
+ """
+ if sys.stdout.isatty():
+ print("\r%s" % message, end="\n" if last_line else "")
+ else:
+ print(message)
+
+
+def expand_chip_name(chip_name):
+ """Change chip name to official form, e.g. `esp32s3beta2` -> `ESP32-S3(beta2)`"""
+ # Put "-" after "esp32"
+ chip_name = re.sub(r"(esp32)(?!$)", r"\1-", chip_name)
+ # Put "()" around "betaN"
+ chip_name = re.sub(r"(beta\d*)", r"(\1)", chip_name)
+ # Uppercase everything before "(betaN)"
+ chip_name = re.sub(r"^[^\(]+", lambda x: x.group(0).upper(), chip_name)
+ return chip_name
+
+
+def strip_chip_name(chip_name):
+ """Strip chip name to normalized form, e.g. `ESP32-S3(beta2)` -> `esp32s3beta2`"""
+ return re.sub(r"[-()]", "", chip_name.lower())
+
+
+class FatalError(RuntimeError):
+ """
+ Wrapper class for runtime errors that aren't caused by internal bugs, but by
+ ESP ROM responses or input content.
+ """
+
+ def __init__(self, message):
+ RuntimeError.__init__(self, message)
+
+ @staticmethod
+ def WithResult(message, result):
+ """
+ Return a fatal error object that appends the hex values of
+ 'result' and its meaning as a string formatted argument.
+ """
+
+ err_defs = {
+ # ROM error codes
+ 0x101: "Out of memory",
+ 0x102: "Invalid argument",
+ 0x103: "Invalid state",
+ 0x104: "Invalid size",
+ 0x105: "Requested resource not found",
+ 0x106: "Operation or feature not supported",
+ 0x107: "Operation timed out",
+ 0x108: "Received response was invalid",
+ 0x109: "CRC or checksum was invalid",
+ 0x10A: "Version was invalid",
+ 0x10B: "MAC address was invalid",
+ # Flasher stub error codes
+ 0xC000: "Bad data length",
+ 0xC100: "Bad data checksum",
+ 0xC200: "Bad blocksize",
+ 0xC300: "Invalid command",
+ 0xC400: "Failed SPI operation",
+ 0xC500: "Failed SPI unlock",
+ 0xC600: "Not in flash mode",
+ 0xC700: "Inflate error",
+ 0xC800: "Not enough data",
+ 0xC900: "Too much data",
+ 0xFF00: "Command not implemented",
+ }
+
+ err_code = struct.unpack(">H", result[:2])
+ message += " (result was {}: {})".format(
+ hexify(result), err_defs.get(err_code[0], "Unknown result")
+ )
+ return FatalError(message)
+
+
+class NotImplementedInROMError(FatalError):
+ """
+ Wrapper class for the error thrown when a particular ESP bootloader function
+ is not implemented in the ROM bootloader.
+ """
+
+ def __init__(self, bootloader, func):
+ FatalError.__init__(
+ self,
+ "%s ROM does not support function %s."
+ % (bootloader.CHIP_NAME, func.__name__),
+ )
+
+
+class NotSupportedError(FatalError):
+ def __init__(self, esp, function_name):
+ FatalError.__init__(
+ self,
+ "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME),
+ )
+
+
+class UnsupportedCommandError(RuntimeError):
+ """
+ Wrapper class for when ROM loader returns an invalid command response.
+
+ Usually this indicates the loader is running in Secure Download Mode.
+ """
+
+ def __init__(self, esp, op):
+ if esp.secure_download_mode:
+ msg = "This command (0x%x) is not supported in Secure Download Mode" % op
+ else:
+ msg = "Invalid (unsupported) command 0x%x" % op
+ RuntimeError.__init__(self, msg)
diff --git a/installer/install_upgrade.bat b/installer/install_upgrade.bat
new file mode 100644
index 0000000..bc95ac2
--- /dev/null
+++ b/installer/install_upgrade.bat
@@ -0,0 +1,10 @@
+@echo off
+echo Your firmware will be upgraded without factory reset.
+echo IF THIS IS YOUR FIRST FLASH ON THIS BOARD PLEASE USE install_with_factory_reset.bat INSTEAD!
+
+set /p port="Enter COM port (for example COM5): "
+
+python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
+
+echo "Firmware flashed"
+pause
\ No newline at end of file
diff --git a/installer/install_upgrade.sh b/installer/install_upgrade.sh
new file mode 100644
index 0000000..81a238e
--- /dev/null
+++ b/installer/install_upgrade.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+echo "Your firmware will be upgraded without factory reset."
+echo "IF THIS IS YOUR FIRST FLASH ON THIS BOARD PLEASE USE install_with_factory_reset.sh INSTEAD!"
+
+read -p "Enter COM port (for example /dev/ttyS5): " port
+
+python3 ./bin/esptool/esptool.py --chip esp32 --port "$port" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
+
+echo "Firmware flashed"
\ No newline at end of file
diff --git a/installer/install_with_factory_reset.bat b/installer/install_with_factory_reset.bat
new file mode 100644
index 0000000..03f318c
--- /dev/null
+++ b/installer/install_with_factory_reset.bat
@@ -0,0 +1,14 @@
+@echo off
+echo Your current configuration will be LOST!!!
+echo Your current configuration will be LOST!!!
+echo Your current configuration will be LOST!!!
+echo If you already have this board flashed with our firmware please use install_upgrade.bat instead!
+
+set /p port="Enter COM port (for example COM5): "
+
+python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 2686976 firmware/spiffs.bin
+
+python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
+
+echo "Firmware flashed"
+pause
\ No newline at end of file
diff --git a/installer/install_with_factory_reset.sh b/installer/install_with_factory_reset.sh
new file mode 100644
index 0000000..e69de29