Skip to content

Commit 46a9e31

Browse files
committed
feat(cmds): Expand input of all functions to file paths, bytes, or file-like objects
1 parent 03b84a1 commit 46a9e31

11 files changed

+291
-198
lines changed

docs/en/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
copyright = "2016 - {}, Espressif Systems (Shanghai) Co., Ltd".format(
2424
datetime.datetime.now().year
2525
)
26+
autodoc_typehints_format = "short"
2627

2728
# The language for content autogenerated by Sphinx. Refer to documentation
2829
# for a list of supported languages.

docs/en/esptool/scripting.rst

+34
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,40 @@ The following example demonstrates running a series of flash memory operations i
9595
- This example doesn't use ``detect_chip()``, but instantiates a ``ESP32ROM`` class directly. This is useful when you know the target chip in advance. In this scenario ``esp.connect()`` is required to establish a connection with the device.
9696
- Multiple operations can be chained together in a single context manager block.
9797

98+
------------
99+
100+
The Public API implements a custom ``ImageSource`` input type, which expands to ``str | bytes | IO[bytes]`` - a path to the firmware image file, an opened file-like object, or the image data as bytes.
101+
102+
As output, the API returns a ``bytes`` object representing the binary image or writes the image to a file if the ``output`` parameter is provided.
103+
104+
The following example converts an ELF file to a flashable binary, prints the image information, and flashes the image. The example demonstrates three different ways to achieve the same result, showcasing the flexibility of the API:
105+
106+
.. code-block:: python
107+
108+
ELF = "firmware.elf"
109+
110+
# var 1 - Loading ELF from a file, not writing binary to a file
111+
bin_file = elf2image(ELF, "esp32c3")
112+
image_info(bin_file)
113+
with detect_chip(PORT) as esp:
114+
attach_flash(esp)
115+
write_flash(esp, [(0, bin_file)])
116+
117+
# var 2 - Loading ELF from an opened file object, not writing binary to a file
118+
with open(ELF, "rb") as elf_file, detect_chip(PORT) as esp:
119+
bin_file = elf2image(elf_file, "esp32c3")
120+
image_info(bin_file)
121+
attach_flash(esp)
122+
write_flash(esp, [(0, bin_file)])
123+
124+
# var 3 - Loading ELF from a file, writing binary to a file
125+
elf2image(ELF, "esp32c3", "image.bin")
126+
image_info("image.bin")
127+
with detect_chip(PORT) as esp:
128+
attach_flash(esp)
129+
write_flash(esp, [(0, "image.bin")])
130+
131+
98132
------------
99133

100134
**The following section provides a detailed reference for the public API functions.**

esptool/__init__.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -627,11 +627,11 @@ def run_cli(ctx):
627627
@click.pass_context
628628
def image_info_cli(ctx, filename):
629629
"""Dump headers from a binary file (bootloader or application)"""
630-
image_info(filename, ctx.obj["chip"])
630+
image_info(filename, chip=None if ctx.obj["chip"] == "auto" else ctx.obj["chip"])
631631

632632

633633
@cli.command("elf2image")
634-
@click.argument("input", type=click.Path(exists=True))
634+
@click.argument("filename", type=click.Path(exists=True))
635635
@click.option(
636636
"--output",
637637
"-o",
@@ -721,14 +721,16 @@ def image_info_cli(ctx, filename):
721721
)
722722
@add_spi_flash_options(allow_keep=False, auto_detect=False)
723723
@click.pass_context
724-
def elf2image_cli(ctx, **kwargs):
724+
def elf2image_cli(ctx, filename, **kwargs):
725725
"""Create an application image from ELF file"""
726+
if ctx.obj["chip"] == "auto":
727+
raise FatalError(
728+
f"Specify the --chip argument (choose from {', '.join(CHIP_LIST)})"
729+
)
726730
append_digest = not kwargs.pop("dont_append_digest", False)
727-
# Default to ESP8266 for backwards compatibility
728-
chip = "esp8266" if ctx.obj["chip"] == "auto" else ctx.obj["chip"]
729731
output = kwargs.pop("output", None)
730732
output = "auto" if output is None else output
731-
elf2image(chip=chip, output=output, append_digest=append_digest, **kwargs)
733+
elf2image(filename, ctx.obj["chip"], output, append_digest=append_digest, **kwargs)
732734

733735

734736
@cli.command("read_mac")
@@ -917,6 +919,10 @@ def read_flash_sfdp_cli(ctx, address, bytes, **kwargs):
917919
@click.pass_context
918920
def merge_bin_cli(ctx, addr_filename, **kwargs):
919921
"""Merge multiple raw binary files into a single file for later flashing"""
922+
if ctx.obj["chip"] == "auto":
923+
raise FatalError(
924+
f"Specify the --chip argument (choose from {', '.join(CHIP_LIST)})"
925+
)
920926
merge_bin(addr_filename, chip=ctx.obj["chip"], **kwargs)
921927

922928

esptool/bin_image.py

+54-61
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
ESP32S3ROM,
3333
ESP8266ROM,
3434
)
35-
from .util import FatalError, byte, pad_to
35+
from .util import FatalError, byte, ImageSource, get_bytes, pad_to
3636

3737

3838
def align_file_position(f, size):
@@ -62,48 +62,43 @@ def intel_hex_to_bin(file: IO[bytes], start_addr: int | None = None) -> IO[bytes
6262
return file
6363

6464

65-
def LoadFirmwareImage(chip, image_file):
65+
def LoadFirmwareImage(chip: str, image_data: ImageSource):
6666
"""
6767
Load a firmware image. Can be for any supported SoC.
6868
6969
ESP8266 images will be examined to determine if they are original ROM firmware
7070
images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images.
7171
72-
Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1)
73-
or ESP8266V2FirmwareImage (v2).
72+
Returns a BaseFirmwareImage subclass.
7473
"""
75-
76-
def select_image_class(f, chip):
77-
chip = re.sub(r"[-()]", "", chip.lower())
78-
if chip != "esp8266":
79-
return {
80-
"esp32": ESP32FirmwareImage,
81-
"esp32s2": ESP32S2FirmwareImage,
82-
"esp32s3": ESP32S3FirmwareImage,
83-
"esp32c3": ESP32C3FirmwareImage,
84-
"esp32c2": ESP32C2FirmwareImage,
85-
"esp32c6": ESP32C6FirmwareImage,
86-
"esp32c61": ESP32C61FirmwareImage,
87-
"esp32c5": ESP32C5FirmwareImage,
88-
"esp32h2": ESP32H2FirmwareImage,
89-
"esp32h21": ESP32H21FirmwareImage,
90-
"esp32p4": ESP32P4FirmwareImage,
91-
"esp32h4": ESP32H4FirmwareImage,
92-
}[chip](f)
93-
else: # Otherwise, ESP8266 so look at magic to determine the image type
94-
magic = ord(f.read(1))
95-
f.seek(0)
96-
if magic == ESPLoader.ESP_IMAGE_MAGIC:
97-
return ESP8266ROMFirmwareImage(f)
98-
elif magic == ESP8266V2FirmwareImage.IMAGE_V2_MAGIC:
99-
return ESP8266V2FirmwareImage(f)
100-
else:
101-
raise FatalError("Invalid image magic number: %d" % magic)
102-
103-
if isinstance(image_file, str):
104-
with open(image_file, "rb") as f:
105-
return select_image_class(f, chip)
106-
return select_image_class(image_file, chip)
74+
data, _ = get_bytes(image_data)
75+
f = io.BytesIO(data)
76+
chip = re.sub(r"[-()]", "", chip.lower())
77+
if chip == "esp8266":
78+
# Look at the magic number to determine the ESP8266 image type
79+
magic = ord(f.read(1))
80+
f.seek(0)
81+
if magic == ESPLoader.ESP_IMAGE_MAGIC:
82+
return ESP8266ROMFirmwareImage(f)
83+
elif magic == ESP8266V2FirmwareImage.IMAGE_V2_MAGIC:
84+
return ESP8266V2FirmwareImage(f)
85+
else:
86+
raise FatalError(f"Invalid image magic number: {magic}")
87+
else:
88+
return {
89+
"esp32": ESP32FirmwareImage,
90+
"esp32s2": ESP32S2FirmwareImage,
91+
"esp32s3": ESP32S3FirmwareImage,
92+
"esp32c3": ESP32C3FirmwareImage,
93+
"esp32c2": ESP32C2FirmwareImage,
94+
"esp32c6": ESP32C6FirmwareImage,
95+
"esp32c61": ESP32C61FirmwareImage,
96+
"esp32c5": ESP32C5FirmwareImage,
97+
"esp32h2": ESP32H2FirmwareImage,
98+
"esp32h21": ESP32H21FirmwareImage,
99+
"esp32p4": ESP32P4FirmwareImage,
100+
"esp32h4": ESP32H4FirmwareImage,
101+
}[chip](f)
107102

108103

109104
class ImageSegment(object):
@@ -1225,11 +1220,10 @@ class ELFFile(object):
12251220
SEG_TYPE_LOAD = 0x01
12261221
LEN_SEG_HEADER = 0x20
12271222

1228-
def __init__(self, name):
1229-
# Load sections from the ELF file
1230-
self.name = name
1231-
with open(self.name, "rb") as f:
1232-
self._read_elf_file(f)
1223+
def __init__(self, data):
1224+
self.data, self.name = get_bytes(data)
1225+
f = io.BytesIO(self.data)
1226+
self._read_elf_file(f)
12331227

12341228
def get_section(self, section_name):
12351229
for s in self.sections:
@@ -1240,6 +1234,7 @@ def get_section(self, section_name):
12401234
def _read_elf_file(self, f):
12411235
# read the ELF file header
12421236
LEN_FILE_HEADER = 0x34
1237+
source = "Image" if self.name is None else f"'{self.name}'"
12431238
try:
12441239
(
12451240
ident,
@@ -1257,25 +1252,23 @@ def _read_elf_file(self, f):
12571252
shnum,
12581253
shstrndx,
12591254
) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER))
1260-
except struct.error as e:
1261-
raise FatalError(
1262-
"Failed to read a valid ELF header from %s: %s" % (self.name, e)
1263-
)
12641255

1256+
except struct.error as e:
1257+
raise FatalError(f"{source} does not have a valid ELF header: {e}")
12651258
if byte(ident, 0) != 0x7F or ident[1:4] != b"ELF":
1266-
raise FatalError("%s has invalid ELF magic header" % self.name)
1259+
raise FatalError(f"{source} has invalid ELF magic header")
12671260
if machine not in [0x5E, 0xF3]:
12681261
raise FatalError(
1269-
"%s does not appear to be an Xtensa or an RISCV ELF file. "
1270-
"e_machine=%04x" % (self.name, machine)
1262+
f"{source} does not appear to be an Xtensa or an RISCV ELF image. "
1263+
f"(e_machine = {machine:#06x})"
12711264
)
12721265
if shentsize != self.LEN_SEC_HEADER:
12731266
raise FatalError(
1274-
"%s has unexpected section header entry size 0x%x (not 0x%x)"
1275-
% (self.name, shentsize, self.LEN_SEC_HEADER)
1267+
f"{source} has unexpected section header entry size {shentsize:#x} "
1268+
f"(not {self.LEN_SEC_HEADER:#x})"
12761269
)
12771270
if shnum == 0:
1278-
raise FatalError("%s has 0 section headers" % (self.name))
1271+
raise FatalError(f"{source} has 0 section headers")
12791272
self._read_sections(f, shoff, shnum, shstrndx)
12801273
self._read_segments(f, _phoff, _phnum, shstrndx)
12811274

@@ -1285,13 +1278,13 @@ def _read_sections(self, f, section_header_offs, section_header_count, shstrndx)
12851278
section_header = f.read(len_bytes)
12861279
if len(section_header) == 0:
12871280
raise FatalError(
1288-
"No section header found at offset %04x in ELF file."
1289-
% section_header_offs
1281+
f"No section header found at offset {section_header_offs:#06x} "
1282+
"in ELF image."
12901283
)
12911284
if len(section_header) != (len_bytes):
12921285
raise FatalError(
1293-
"Only read 0x%x bytes from section header (expected 0x%x.) "
1294-
"Truncated ELF file?" % (len(section_header), len_bytes)
1286+
f"Only read {len(section_header):#x} bytes from section header "
1287+
f"(expected {len_bytes:#x}). Truncated ELF image?"
12951288
)
12961289

12971290
# walk through the section header and extract all sections
@@ -1347,13 +1340,13 @@ def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx)
13471340
segment_header = f.read(len_bytes)
13481341
if len(segment_header) == 0:
13491342
raise FatalError(
1350-
"No segment header found at offset %04x in ELF file."
1351-
% segment_header_offs
1343+
f"No segment header found at offset {segment_header_offs:#06x} "
1344+
"in ELF image."
13521345
)
13531346
if len(segment_header) != (len_bytes):
13541347
raise FatalError(
1355-
"Only read 0x%x bytes from segment header (expected 0x%x.) "
1356-
"Truncated ELF file?" % (len(segment_header), len_bytes)
1348+
f"Only read {len(segment_header):#x} bytes from segment header "
1349+
f"(expected {len_bytes:#x}). Truncated ELF image?"
13571350
)
13581351

13591352
# walk through the segment header and extract all segments
@@ -1389,6 +1382,6 @@ def read_data(offs, size):
13891382
def sha256(self):
13901383
# return SHA256 hash of the input ELF file
13911384
sha256 = hashlib.sha256()
1392-
with open(self.name, "rb") as f:
1393-
sha256.update(f.read())
1385+
f = io.BytesIO(self.data)
1386+
sha256.update(f.read())
13941387
return sha256.digest()

esptool/cli_util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def convert(
170170
if sector_start < end:
171171
raise click.BadParameter(
172172
f"Detected overlap at address: "
173-
f"0x{address:x} for file: {argfile.name}",
173+
f"{address:#x} for file: {argfile.name}",
174174
)
175175
end = sector_end
176176
return pairs

0 commit comments

Comments
 (0)