feat(image_info): Deprecate the --version 1 output format

BREAKING CHANGE
This commit is contained in:
Radim Karniš
2025-01-15 16:25:24 +01:00
parent 83613c8518
commit 3f625c39ad
8 changed files with 229 additions and 287 deletions

View File

@@ -189,4 +189,4 @@ The file is padded with zeros until its size is one byte less than a multiple of
Analyzing a Binary Image
------------------------
To analyze a binary image and get a complete summary of its headers and segments, use the :ref:`image_info <image-info>` command with the ``--version 2`` option.
To analyze a binary image and get a complete summary of its headers and segments, use the :ref:`image_info <image-info>` command.

View File

@@ -225,15 +225,13 @@ By default, ``elf2image`` uses the sections in the ELF file to generate each seg
Output .bin Image Details: image_info
-------------------------------------
The ``image_info`` command outputs some information (load addresses, sizes, etc) about a ``.bin`` file created by ``elf2image``. Command also supports ``.hex`` file created by ``merge_bin`` command from supported ``.bin`` files.
To view more information about the image, such as set flash size, frequency and mode, or extended header information, use the ``--version 2`` option. This extended output will become the default in a future major release.
The ``image_info`` command outputs some information (load addresses, segment sizes, set flash size, frequency, and mode, extended header information, etc) about a ``.bin`` file created by ``elf2image``. Command also supports ``.hex`` file created by ``merge_bin`` command from supported ``.bin`` files.
This information corresponds to the headers described in :ref:`image-format`.
::
esptool.py image_info --version 2 my_esp_app.bin
esptool.py image_info my_esp_app.bin
.. only:: not esp8266

View File

@@ -51,5 +51,6 @@ More Information
Troubleshooting <troubleshooting>
Contribute <contributing>
Versions <versions>
Migration Guide <migration-guide>
Resources <resources>
About <about>

View File

@@ -0,0 +1,23 @@
.. _migration:
esptool.py ``v5`` Migration Guide
=================================
This document describes the breaking changes made to esptool.py in the major release ``v5``. It provides guidance on adapting existing workflows and scripts to ensure compatibility when updating from ``v4.*``.
``image_info`` Output Format Change
***********************************
The output format of the :ref:`image_info <image-info>` command has been **updated in v5**. The original format (``--version 1``) is **deprecated** and replaced by the updated format (``--version 2``). The ``--version`` argument has been **removed entirely**, and the new format is now the default and only option.
**Changes in the New Format:**
- Improved readability and structure
- Additional metadata fields for better debugging and analysis
- Consistent formatting for all ESP chip variants
**Migration Steps:**
1. Update any scripts or tools that parse the ``image_info`` output to use the new format
2. Remove any ``--version`` arguments from ``image_info`` commands

View File

@@ -402,13 +402,6 @@ def main(argv=None, esp=None):
parser_image_info.add_argument(
"filename", help="Image file to parse", action=AutoHex2BinAction
)
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"

View File

@@ -769,219 +769,6 @@ def write_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(
f"WP pin: {image.wp_pin:#02x}",
*["(disabled)"] if image.wp_pin == image.WP_PIN_DISABLED else [],
)
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,
)
)
try:
chip = next(
chip
for chip in CHIP_DEFS.values()
if getattr(chip, "IMAGE_CHIP_ID", None) == image.chip_id
)
print(f"Chip ID: {image.chip_id} ({chip.CHIP_NAME})")
except StopIteration:
print(f"Chip ID: {image.chip_id} (Unknown 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
bootloader_desc = None
for idx, seg in enumerate(image.segments):
segs = seg.get_memory_type(image)
seg_name = ", ".join(segs)
# The DROM segment starts with the esp_app_desc_t struct
if "DROM" in segs and app_desc is None:
app_desc = seg.data[:256]
elif "DRAM" in segs:
# The DRAM segment starts with the esp_bootloader_desc_t struct
if len(seg.data) >= 80:
bootloader_desc = seg.data[:80]
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 = "<II" + "8s" + "32s32s16s16s32s32sHHB" + "3s" + "72s"
(
magic_word,
secure_version,
reserv1,
version,
project_name,
time,
date,
idf_ver,
app_elf_sha256,
min_efuse_blk_rev_full,
max_efuse_blk_rev_full,
mmu_page_size,
reserv3,
reserv2,
) = struct.unpack(APP_DESC_STRUCT_FMT, app_desc)
if magic_word == 0xABCD5432:
print()
title = "Application information"
print(title)
print("=" * len(title))
print(f'Project name: {project_name.decode("utf-8")}')
print(f'App version: {version.decode("utf-8")}')
print(f'Compile time: {date.decode("utf-8")} {time.decode("utf-8")}')
print(f"ELF file SHA256: {hexify(app_elf_sha256, uppercase=False)}")
print(f'ESP-IDF: {idf_ver.decode("utf-8")}')
print(
f"Minimal eFuse block revision: {min_efuse_blk_rev_full // 100}.{min_efuse_blk_rev_full % 100}"
)
print(
f"Maximal eFuse block revision: {max_efuse_blk_rev_full // 100}.{max_efuse_blk_rev_full % 100}"
)
# MMU page size is only available in ESP-IDF v5.4 and later
# regex matches major and minor version numbers, idf_ver can look like "v5.4.1-dirty"
ver = re.match(r"v(\d+)\.(\d+)", idf_ver.decode("utf-8"))
if ver:
major, minor = ver.groups()
if int(major) >= 5 and int(minor) >= 4:
print(f"MMU page size: {2 ** mmu_page_size // 1024} KB")
print(f"Secure version: {secure_version}")
elif bootloader_desc:
BOOTLOADER_DESC_STRUCT_FMT = "<B" + "3s" + "I32s24s" + "16s"
(
magic_byte,
reserved,
version,
idf_ver,
date_time,
reserved2,
) = struct.unpack(BOOTLOADER_DESC_STRUCT_FMT, bootloader_desc)
if magic_byte == 80:
print()
title = "Bootloader information"
print(title)
print("=" * len(title))
print(f"Bootloader version: {version}")
print(f'ESP-IDF: {idf_ver.decode("utf-8")}')
print(f'Compile time: {date_time.decode("utf-8")}')
print(f"File size: {get_file_size(args.filename)} (bytes)")
with open(args.filename, "rb") as f:
# magic number
@@ -1020,32 +807,133 @@ def image_info(args):
image = LoadFirmwareImage(args.chip, args.filename)
if args.version == "2":
v2()
return
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("Image version: {}".format(image.version))
print()
title = f"{args.chip.upper()} image header".format()
print(title)
print("=" * len(title))
print(f"Image version: {image.version}")
print(
"Entry point: {:8x}".format(image.entrypoint)
f"Entry point: {image.entrypoint:#8x}"
if image.entrypoint != 0
else "Entry point not set"
)
print("{} segments".format(len(image.segments)))
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(
f"Flash size: {flash_s}"
if flash_s is not None
else f"WARNING: Invalid flash size ({flash_s_bits:#02x})"
)
# 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(
f"Flash freq: {flash_fr}"
if flash_fr is not None
else f"WARNING: Invalid flash frequency ({flash_fr_bits:#02x})"
)
# Flash mode
flash_mode = get_key_from_value(FLASH_MODES, image.flash_mode)
print(
f"Flash mode: {flash_mode.upper()}"
if flash_mode is not None
else f"WARNING: Invalid flash mode ({image.flash_mode})"
)
# Extended header (ESP32 and later only)
if args.chip != "esp8266":
print()
title = f"{args.chip.upper()} extended image header"
print(title)
print("=" * len(title))
print(
f"WP pin: {image.wp_pin:#02x}",
*["(disabled)"] if image.wp_pin == image.WP_PIN_DISABLED else [],
)
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,
)
)
try:
chip = next(
chip
for chip in CHIP_DEFS.values()
if getattr(chip, "IMAGE_CHIP_ID", None) == image.chip_id
)
print(f"Chip ID: {image.chip_id} ({chip.CHIP_NAME})")
except StopIteration:
print(f"Chip ID: {image.chip_id} (Unknown 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()
idx = 0
for seg in image.segments:
idx += 1
# 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
bootloader_desc = None
for idx, seg in enumerate(image.segments):
segs = seg.get_memory_type(image)
seg_name = ",".join(segs)
print("Segment {}: {} [{}]".format(idx, seg, seg_name))
seg_name = ", ".join(segs)
# The DROM segment starts with the esp_app_desc_t struct
if "DROM" in segs and app_desc is None:
app_desc = seg.data[:256]
elif "DRAM" in segs:
# The DRAM segment starts with the esp_bootloader_desc_t struct
if len(seg.data) >= 80:
bootloader_desc = seg.data[:80]
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(
"Checksum: 0x{:02x} ({})".format(
image.checksum,
(
"valid"
if image.checksum == calc_checksum
else "invalid - calculated {:02x}".format(calc_checksum)
else f"invalid - calculated 0x{calc_checksum:02x}"
),
)
)
@@ -1057,10 +945,76 @@ def image_info(args):
hexify(image.calc_digest, uppercase=False),
"valid" if is_valid else "invalid",
)
print("Validation Hash: {}".format(digest_msg))
print(f"Validation hash: {digest_msg}")
except AttributeError:
pass # ESP8266 image has no append_digest field
if app_desc:
APP_DESC_STRUCT_FMT = "<II" + "8s" + "32s32s16s16s32s32sHHB" + "3s" + "72s"
(
magic_word,
secure_version,
reserv1,
version,
project_name,
time,
date,
idf_ver,
app_elf_sha256,
min_efuse_blk_rev_full,
max_efuse_blk_rev_full,
mmu_page_size,
reserv3,
reserv2,
) = struct.unpack(APP_DESC_STRUCT_FMT, app_desc)
if magic_word == 0xABCD5432:
print()
title = "Application information"
print(title)
print("=" * len(title))
print(f'Project name: {project_name.decode("utf-8")}')
print(f'App version: {version.decode("utf-8")}')
print(f'Compile time: {date.decode("utf-8")} {time.decode("utf-8")}')
print(f"ELF file SHA256: {hexify(app_elf_sha256, uppercase=False)}")
print(f'ESP-IDF: {idf_ver.decode("utf-8")}')
print(
f"Minimal eFuse block revision: {min_efuse_blk_rev_full // 100}.{min_efuse_blk_rev_full % 100}"
)
print(
f"Maximal eFuse block revision: {max_efuse_blk_rev_full // 100}.{max_efuse_blk_rev_full % 100}"
)
# MMU page size is only available in ESP-IDF v5.4 and later
# regex matches major and minor version numbers, idf_ver can look like "v5.4.1-dirty"
ver = re.match(r"v(\d+)\.(\d+)", idf_ver.decode("utf-8"))
if ver:
major, minor = ver.groups()
if int(major) >= 5 and int(minor) >= 4:
print(f"MMU page size: {2 ** mmu_page_size // 1024} KB")
print(f"Secure version: {secure_version}")
elif bootloader_desc:
BOOTLOADER_DESC_STRUCT_FMT = "<B" + "3s" + "I32s24s" + "16s"
(
magic_byte,
reserved,
version,
idf_ver,
date_time,
reserved2,
) = struct.unpack(BOOTLOADER_DESC_STRUCT_FMT, bootloader_desc)
if magic_byte == 80:
print()
title = "Bootloader information"
print(title)
print("=" * len(title))
print(f"Bootloader version: {version}")
print(f'ESP-IDF: {idf_ver.decode("utf-8")}')
print(f'Compile time: {date_time.decode("utf-8")}')
def make_image(args):
print("Creating {} image...".format(args.chip))

View File

@@ -25,7 +25,7 @@ def read_image(filename):
@pytest.mark.host_test
class TestImageInfo:
def run_image_info(self, chip, file, version=None):
def run_image_info(self, chip, file):
"""Runs image_info on a binary file.
Returns the command output.
Filenames are relative to the 'test/images' directory.
@@ -39,8 +39,6 @@ class TestImageInfo:
chip,
"image_info",
]
if version is not None:
cmd += ["--version", str(version)]
# if path was passed use the whole path
# if file does not exists try to use file from IMAGES_DIR directory
cmd += [file] if os.path.isfile(file) else ["".join([IMAGES_DIR, os.sep, file])]
@@ -58,28 +56,8 @@ class TestImageInfo:
print(e.output)
raise
def test_v1_esp32(self):
out = self.run_image_info("esp32", "bootloader_esp32.bin")
assert "Entry point: 4009816c" in out, "Wrong entry point"
assert "Checksum: 83 (valid)" in out, "Invalid checksum"
assert "4 segments" in out, "Wrong number of segments"
assert (
"Segment 3: len 0x01068 load 0x40078000 file_offs 0x00000b64 [CACHE_APP]"
in out
), "Wrong segment info"
def test_v1_esp8266(self):
out = self.run_image_info("esp8266", ESP8266_BIN)
assert "Image version: 1" in out, "Wrong image version"
assert "Entry point: 40101844" in out, "Wrong entry point"
assert "Checksum: 6b (valid)" in out, "Invalid checksum"
assert "1 segments" in out, "Wrong number of segments"
assert (
"Segment 1: len 0x00014 load 0x40100000 file_offs 0x00000008 [IRAM]" in out
), "Wrong segment info"
def test_v2_esp32c3(self):
out = self.run_image_info("esp32c3", "bootloader_esp32c3.bin", "2")
def test_esp32c3(self):
out = self.run_image_info("esp32c3", "bootloader_esp32c3.bin")
# Header
assert "Entry point: 0x403c0000" in out, "Wrong entry point"
@@ -117,8 +95,8 @@ class TestImageInfo:
if ex_hdr[15] == 1: # Hash appended
assert "Validation hash: 4faeab1bd3fd" in out, "Invalid hash"
def test_v2_esp8266(self):
out = self.run_image_info("esp8266", ESP8266_BIN, "2")
def test_esp8266(self):
out = self.run_image_info("esp8266", ESP8266_BIN)
assert "Image version: 1" in out, "Wrong image version"
assert "Entry point: 0x40101844" in out, "Wrong entry point"
assert "Flash size: 512KB" in out, "Wrong flash size"
@@ -129,45 +107,40 @@ class TestImageInfo:
assert "0 0x00014 0x40100000 0x00000008 IRAM" in out, "Wrong segment info"
def test_image_type_detection(self):
# ESP8266, version 1 and 2
out = self.run_image_info("auto", ESP8266_BIN, "1")
assert "Detected image type: ESP8266" in out
assert "Segment 1: len 0x00014" in out
out = self.run_image_info("auto", ESP8266_BIN, "2")
# ESP8266
out = self.run_image_info("auto", ESP8266_BIN)
assert "Detected image type: ESP8266" in out
assert "Flash freq: 40m" in out
out = self.run_image_info("auto", "esp8266_deepsleep.bin", "2")
out = self.run_image_info("auto", "esp8266_deepsleep.bin")
assert "Detected image type: ESP8266" in out
# ESP32, with and without detection
out = self.run_image_info("auto", "bootloader_esp32.bin", "2")
out = self.run_image_info("auto", "bootloader_esp32.bin")
assert "Detected image type: ESP32" in out
out = self.run_image_info(
"auto", "ram_helloworld/helloworld-esp32_edit.bin", "2"
)
out = self.run_image_info("auto", "ram_helloworld/helloworld-esp32_edit.bin")
assert "Detected image type: ESP32" in out
out = self.run_image_info("esp32", "bootloader_esp32.bin", "2")
out = self.run_image_info("esp32", "bootloader_esp32.bin")
assert "Detected image type: ESP32" not in out
# ESP32-C3
out = self.run_image_info("auto", "bootloader_esp32c3.bin", "2")
out = self.run_image_info("auto", "bootloader_esp32c3.bin")
assert "Detected image type: ESP32-C3" in out
# ESP32-S3
out = self.run_image_info("auto", "esp32s3_header.bin", "2")
out = self.run_image_info("auto", "esp32s3_header.bin")
assert "Detected image type: ESP32-S3" in out
def test_invalid_image_type_detection(self, capsys):
with pytest.raises(subprocess.CalledProcessError):
# Invalid image
self.run_image_info("auto", "one_kb.bin", "2")
self.run_image_info("auto", "one_kb.bin")
assert (
"This is not a valid image (invalid magic number: 0xed)"
in capsys.readouterr().out
)
def test_application_info(self):
out = self.run_image_info("auto", "esp_idf_blink_esp32s2.bin", "2")
out = self.run_image_info("auto", "esp_idf_blink_esp32s2.bin")
assert "Application information" in out
assert "Project name: blink" in out
assert "App version: qa-test-v5.0-20220830-4-g4532e6" in out
@@ -178,15 +151,15 @@ class TestImageInfo:
assert "cd0dab311febb0a3ea79eaa223ac2b0" in out
assert "ESP-IDF: v5.0-beta1-427-g4532e6e0b2-dirt" in out
# No application info in image
out = self.run_image_info("auto", "bootloader_esp32.bin", "2")
out = self.run_image_info("auto", "bootloader_esp32.bin")
assert "Application information" not in out
out = self.run_image_info("auto", ESP8266_BIN, "2")
out = self.run_image_info("auto", ESP8266_BIN)
assert "Application information" not in out
def test_bootloader_info(self):
# This bootloader binary is built from "hello_world" project
# with default settings, IDF version is v5.2.
out = self.run_image_info("esp32", "bootloader_esp32_v5_2.bin", "2")
out = self.run_image_info("esp32", "bootloader_esp32_v5_2.bin")
assert "File size: 26768 (bytes)" in out
assert "Bootloader information" in out
assert "Bootloader version: 1" in out
@@ -219,7 +192,7 @@ class TestImageInfo:
fd, file = tempfile.mkstemp(suffix=".hex")
try:
convert_bin2hex(file)
out = self.run_image_info("esp32", file, "2")
out = self.run_image_info("esp32", file)
assert "File size: 26768 (bytes)" in out
assert "Bootloader information" in out
assert "Bootloader version: 1" in out

View File

@@ -128,11 +128,11 @@ class BaseTestCase:
print(e.output)
raise
assert re.search(
r"Checksum: [a-fA-F0-9]{2} \(valid\)", output
r"Checksum: 0x[a-fA-F0-9]{2} \(valid\)", output
), "Checksum calculation should be valid"
if assert_sha:
assert re.search(
r"Validation Hash: [a-fA-F0-9]{64} \(valid\)", output
r"Validation hash: [a-fA-F0-9]{64} \(valid\)", output
), "SHA256 should be valid"
assert (
"warning" not in output.lower()