Skip to content

Commit e1a3422

Browse files
authored
Merge branch 'espressif:master' into pioarduino
2 parents 5323e4a + 605c80b commit e1a3422

File tree

9 files changed

+163
-49
lines changed

9 files changed

+163
-49
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@
2020
</div>
2121
<hr>
2222

23+
## v5.0.2 (2025-07-30)
24+
25+
### 🐛 Bug Fixes
26+
27+
- **esp32-c3**: Disable flasher stub when Secure Boot is active *(Radim Karniš - 1f1ea9a)*
28+
- **esp32-s3**: Allow stub flasher execution with active Secure Boot *(Radim Karniš - 7ba285b)*
29+
- **espefuse**: Handle error in burn-efuse command when no arguments are provided *(Peter Dragun - 0f32306)*
30+
- Fix buffering issues with CP2102 converter causing connection failures *(Jaroslav Burian - 5338ea0)*
31+
- Fix compatibility with Click 8.2.0+ *(Peter Dragun - 524825e)*
32+
- Fix --port-filter argument parsing *(Peter Dragun - b53a16c)*
33+
34+
### 🔧 Code Refactoring
35+
36+
- **elf2image**: Use common MMU page size configuration function for ESP32-H4 *(Jaroslav Burian - 977ff44)*
37+
38+
2339
## v5.0.1 (2025-07-15)
2440

2541
### 🐛 Bug Fixes

espefuse/efuse/base_operations.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,22 @@
2323
from .emulate_efuse_controller_base import EmulateEfuseControllerBase
2424

2525

26-
class EfuseValuePairArg(click.Argument):
26+
class EfuseArgument(click.Argument):
27+
def make_metavar(self, ctx: click.Context | None = None) -> str:
28+
"""Compatibility layer for Click 8.2.0+; which now requires a ctx parameter."""
29+
try:
30+
return super().make_metavar(ctx) # type: ignore
31+
except TypeError:
32+
# Fall back to the old signature (pre-Click 8.2.0)
33+
return super().make_metavar() # type: ignore
34+
35+
36+
class EfuseValuePairArg(EfuseArgument):
2737
def __init__(self, *args, **kwargs):
2838
super().__init__(*args, **kwargs)
2939

30-
def make_metavar(self) -> str:
31-
return f"[{super().make_metavar()}] ..."
40+
def make_metavar(self, ctx=None) -> str:
41+
return f"[{super().make_metavar(ctx)}] ..."
3242

3343
def type_cast_value(self, ctx: click.Context, value: list[str]):
3444
return self.type.convert(value, None, ctx)
@@ -52,7 +62,9 @@ def check_efuse_name(efuse_name: str):
5262

5363
# Handle single value case (eFuse name only)
5464
efuse_value_pairs = {}
55-
if len(value) > 1:
65+
if len(value) == 0:
66+
raise click.BadParameter("Missing eFuse name and value pair.")
67+
elif len(value) > 1:
5668
if len(value) % 2:
5769
raise click.BadParameter(
5870
f"The list does not have a valid pair (name value) {value}"
@@ -82,17 +94,17 @@ def convert(self, value: str, param: click.Parameter | None, ctx: click.Context)
8294
return base_fields.CheckArgValue(ctx.obj["efuses"], "CUSTOM_MAC")(value)
8395

8496

85-
class TupleParameter(click.Argument):
97+
class TupleParameter(EfuseArgument):
8698
def __init__(self, *args, **kwargs):
8799
self.max_arity = kwargs.pop("max_arity", None)
88100
super().__init__(*args, **kwargs)
89101

90-
def make_metavar(self) -> str:
102+
def make_metavar(self, ctx=None) -> str:
91103
if self.nargs == 1:
92-
return super().make_metavar() # type: ignore
104+
return super().make_metavar(ctx) # type: ignore
93105
if self.max_arity is None:
94-
return f"[{super().make_metavar()}] ..."
95-
return f"[{super().make_metavar()}] ... (max {self.max_arity} groups)"
106+
return f"[{super().make_metavar(ctx)}] ..."
107+
return f"[{super().make_metavar(ctx)}] ... (max {self.max_arity} groups)"
96108

97109
def type_cast_value(self, ctx: click.Context, value: list[str]) -> tuple[Any, ...]:
98110
# This is by default eating all options, so we need to check for help option

esptool/__init__.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"write_mem",
3232
]
3333

34-
__version__ = "5.0.1"
34+
__version__ = "5.0.2"
3535

3636
import os
3737
import shlex
@@ -930,11 +930,6 @@ def erase_flash_cli(ctx, force, **kwargs):
930930
)
931931
@click.argument("address", type=AnyIntType())
932932
@click.argument("size", type=AutoSizeType())
933-
@click.option(
934-
"--force",
935-
is_flag=True,
936-
help="Erase region even if security features are enabled. Use with caution!",
937-
)
938933
@add_spi_connection_arg
939934
@click.pass_context
940935
def erase_region_cli(ctx, address, size, force, **kwargs):

esptool/bin_image.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ class ESP32FirmwareImage(BaseFirmwareImage):
710710
can be placed in the normal image (just @ MMU page size padded offsets).
711711
"""
712712

713-
ROM_LOADER = ESP32ROM
713+
ROM_LOADER: type[ESPLoader] = ESP32ROM
714714

715715
# ROM bootloader will read the wp_pin field if SPI flash
716716
# pins are remapped via flash. IDF actually enables QIO only
@@ -1211,7 +1211,7 @@ class ESP32C2FirmwareImage(ESP32FirmwareImage):
12111211
class ESP32C6FirmwareImage(ESP32FirmwareImage):
12121212
"""ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
12131213

1214-
ROM_LOADER = ESP32C6ROM
1214+
ROM_LOADER: type[ESPLoader] = ESP32C6ROM
12151215
MMU_PAGE_SIZE_CONF = (8192, 16384, 32768, 65536)
12161216

12171217

@@ -1236,19 +1236,11 @@ class ESP32C5FirmwareImage(ESP32FirmwareImage):
12361236
ESP32C5ROM.BOOTLOADER_IMAGE = ESP32C5FirmwareImage
12371237

12381238

1239-
class ESP32H4FirmwareImage(ESP32FirmwareImage):
1240-
"""ESP32H4 Firmware Image almost exactly the same as ESP32FirmwareImage"""
1239+
class ESP32H4FirmwareImage(ESP32C6FirmwareImage):
1240+
"""ESP32H4 Firmware Image almost exactly the same as ESP32C6FirmwareImage"""
12411241

12421242
ROM_LOADER = ESP32H4ROM
12431243

1244-
def set_mmu_page_size(self, size):
1245-
if size not in [8192, 16384, 32768, 65536]:
1246-
raise FatalError(
1247-
"{} bytes is not a valid ESP32-H4 page size, "
1248-
"select from 64KB, 32KB, 16KB, 8KB.".format(size)
1249-
)
1250-
self.IROM_ALIGN = size
1251-
12521244

12531245
ESP32H4ROM.BOOTLOADER_IMAGE = ESP32H4FirmwareImage
12541246

esptool/cli_util.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ class AddrFilenamePairType(click.Path):
155155

156156
name = "addr-filename-pair"
157157

158-
def get_metavar(self, param):
158+
def get_metavar(
159+
self, param: click.Parameter | None, ctx: click.Context | None = None
160+
):
159161
return "<address> <filename>"
160162

161163
def convert(
@@ -294,13 +296,23 @@ def __init__(self, *args, **kwargs):
294296
self._eat_all_parser = None
295297
# Set the metavar dynamically based on the type's metavar
296298
if self.type and hasattr(self.type, "name"):
297-
self.metavar = f"[{self.type.get_metavar(None) or self.type.name.upper()}]"
299+
self.metavar = f"[{self._get_metavar() or self.type.name.upper()}]"
300+
301+
def _get_metavar(self):
302+
"""Get the metavar for the option. Wrapper for compatibility reasons.
303+
In Click 8.2.0+, the `get_metavar` requires new parameter `ctx`.
304+
"""
305+
try:
306+
ctx = click.get_current_context(silent=True)
307+
return self.type.get_metavar(None, ctx)
308+
except TypeError:
309+
return self.type.get_metavar(None)
298310

299311
def add_to_parser(self, parser, ctx):
300312
def parser_process(value, state):
301313
# Method to hook into the parser.process
302314
done = False
303-
value = [value]
315+
values = [value]
304316
# Grab everything up to the next option/command
305317
while state.rargs and not done:
306318
for prefix in self._eat_all_parser.prefixes:
@@ -310,10 +322,16 @@ def parser_process(value, state):
310322
if state.rargs[0] in self._commands_list:
311323
done = True
312324
if not done:
313-
value.append(state.rargs.pop(0))
325+
values.append(state.rargs.pop(0))
314326

315327
# Call the original parser process method on the rest of the arguments
316-
self._previous_parser_process(value, state)
328+
if self.multiple:
329+
# If multiple options can be used, Click does not support extending the
330+
# value; as the 'value' is list, we need to process each item separately
331+
for v in values:
332+
self._previous_parser_process(v, state)
333+
else:
334+
self._previous_parser_process(values, state)
317335

318336
retval = super(OptionEatAll, self).add_to_parser(parser, ctx)
319337
for name in self.opts:
@@ -379,7 +397,7 @@ def arg_auto_int(x: str) -> int:
379397

380398

381399
def parse_port_filters(
382-
value: list[str],
400+
value: tuple[str],
383401
) -> tuple[list[int], list[int], list[str], list[str]]:
384402
"""Parse port filter arguments into separate lists for each filter type"""
385403
filterVids = []
@@ -389,7 +407,7 @@ def parse_port_filters(
389407
for f in value:
390408
kvp = f.split("=")
391409
if len(kvp) != 2:
392-
FatalError("Option --port-filter argument must consist of key=value.")
410+
raise FatalError("Option --port-filter argument must consist of key=value.")
393411
if kvp[0] == "vid":
394412
filterVids.append(arg_auto_int(kvp[1]))
395413
elif kvp[0] == "pid":

esptool/cmds.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,11 @@ def run_stub(esp: ESPLoader) -> ESPLoader:
16261626
"Stub flasher is not supported in Secure Download Mode, "
16271627
"it has been disabled. Set --no-stub to suppress this warning."
16281628
)
1629+
elif esp.CHIP_NAME == "ESP32-C3" and esp.get_secure_boot_enabled():
1630+
log.warning(
1631+
"Stub flasher is not supported on ESP32-C3 with Secure Boot, "
1632+
"it has been disabled. Set --no-stub to suppress this warning."
1633+
)
16291634
elif not esp.IS_STUB and esp.stub_is_disabled:
16301635
log.warning(
16311636
"Stub flasher has been disabled for compatibility, "

esptool/loader.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -502,10 +502,35 @@ def command(
502502
if op is None or op_ret == op:
503503
return val, data
504504
if byte(data, 0) != 0 and byte(data, 1) == self.ROM_INVALID_RECV_MSG:
505-
# Unsupported read_reg can result in
505+
506+
def drain_input_buffer(buffering_time=0.2):
507+
"""
508+
Actively drain the input buffer by reading data
509+
for a specified time. Simple approach for some
510+
drivers that have issues with the buffer flushing.
511+
512+
Args:
513+
buffering_time: Time in seconds to wait for
514+
the buffer to fill.
515+
"""
516+
time.sleep(buffering_time)
517+
original_timeout = self._port.timeout
518+
# Set a very short timeout for draining
519+
self._port.timeout = 0.001
520+
521+
# Unsupported command response is sent 8 times and has
522+
# 14 bytes length including delimiter 0xC0 bytes.
523+
# At least part of it is read as a command response,
524+
# but to be safe, read it all.
525+
self._port.read(14 * 8)
526+
527+
# Restore original timeout
528+
self._port.timeout = original_timeout
529+
self.flush_input()
530+
531+
# Unsupported command can result in
506532
# more than one error response for some reason
507-
time.sleep(0.2) # Wait for input buffer to fill
508-
self.flush_input() # Flush input buffer of hanging response
533+
drain_input_buffer(0.2)
509534
raise UnsupportedCommandError(self, op)
510535

511536
finally:
@@ -1259,6 +1284,10 @@ def run_stub(self, stub: StubFlasher | None = None) -> "ESPLoader":
12591284
log.print("Stub flasher is already running. No upload is necessary.")
12601285
return self.STUB_CLASS(self) if self.STUB_CLASS is not None else self
12611286

1287+
secure_boot_workflow = (
1288+
self.CHIP_NAME == "ESP32-S3" and self.get_secure_boot_enabled()
1289+
)
1290+
12621291
# Upload
12631292
log.print("Uploading stub flasher...")
12641293
for field in [stub.text, stub.data]:
@@ -1271,8 +1300,24 @@ def run_stub(self, stub: StubFlasher | None = None) -> "ESPLoader":
12711300
from_offs = seq * self.ESP_RAM_BLOCK
12721301
to_offs = from_offs + self.ESP_RAM_BLOCK
12731302
self.mem_block(field[from_offs:to_offs], seq)
1303+
12741304
log.print("Running stub flasher...")
1275-
self.mem_finish(stub.entry)
1305+
if not secure_boot_workflow:
1306+
self.mem_finish(stub.entry)
1307+
else:
1308+
# Bug in ESP32-S3 ROM prevents stub execution if secure boot is enabled
1309+
# Hijack the `read` function in ROM to point to the stub entrypoint
1310+
# got with GDB - p &rom_spiflash_legacy_funcs.read
1311+
rom_spiflash_legacy_funcs_read_ptr = 0x3FCEF688
1312+
self.mem_finish(0) # Finish uploading to RAM but don't run the stub yet
1313+
stored_read_pointer = self.read_reg(rom_spiflash_legacy_funcs_read_ptr)
1314+
self.write_reg(rom_spiflash_legacy_funcs_read_ptr, stub.entry)
1315+
self.command( # Trigger the `read` in ROM to jump to the stub entrypoint
1316+
self.ESP_CMDS["READ_FLASH_SLOW"],
1317+
struct.pack("<II", 0, 0),
1318+
wait_response=False,
1319+
)
1320+
12761321
try:
12771322
p = self.read()
12781323
except StopIteration:
@@ -1284,6 +1329,9 @@ def run_stub(self, stub: StubFlasher | None = None) -> "ESPLoader":
12841329

12851330
if p != b"OHAI":
12861331
raise FatalError(f"Failed to start stub flasher. Unexpected response: {p}")
1332+
if secure_boot_workflow:
1333+
# Restore the original `read` function pointer
1334+
self.write_reg(rom_spiflash_legacy_funcs_read_ptr, stored_read_pointer)
12871335
log.stage(finish=True)
12881336
log.print("Stub flasher running.")
12891337
return self.STUB_CLASS(self) if self.STUB_CLASS is not None else self

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"PyYAML>=5.1",
3939
"intelhex",
4040
"rich_click",
41-
"click<8.2.0",
41+
"click<9",
4242
]
4343

4444
[project.urls]
@@ -78,7 +78,7 @@
7878

7979
[tool.commitizen]
8080
name = "czespressif"
81-
version = "5.0.1"
81+
version = "5.0.2"
8282
update_changelog_on_bump = true
8383
tag_format = "v$version"
8484
changelog_start_rev = "v4.2.1"

0 commit comments

Comments
 (0)