feat(esptool): Add option to retry connection in a loop

`esptool` frequently fails when trying to open the serial port of a device
which deep-sleeps often:

$ esptool.py --chip esp32s3 -p /dev/cu.usbmodem6101 [...] write_flash foo.bin
Serial port /dev/cu.usbmodem6101

A fatal error occurred: Could not open /dev/cu.usbmodem6101, the port is busy or doesn't exist.
([Errno 35] Could not exclusively lock port [...]: [Errno 35] Resource temporarily unavailable)

This makes developers add unnecessarily long sleeps when the main CPU is awake, in order to give
`esptool` the chance to find the serial port.

This PR adds a new env variable `DEFAULT_OPEN_PORT_ATTEMPTS` and cfg file entry `retry_open_serial`
which attempts to open the port selected number of times (optionaly indefinitely) until the device
shows up:

$ export DEFAULT_OPEN_PORT_ATTEMPTS=1
$ esptool.py --chip esp32s3 -p /dev/cu.usbmodem6101 [...] write_flash foo.bin
Serial port /dev/cu.usbmodem6101
[Errno 35] Could not exclusively lock port [...]: [Errno 35] Resource temporarily unavailable
Retrying to open port .........................
Connecting....
Chip is ESP32-S3 (QFN56) (revision v0.2)
[...]

Closes https://github.com/espressif/esptool/pull/995
This commit is contained in:
Alfonso Acosta
2024-07-17 01:27:21 +02:00
committed by Peter Dragun
parent b3022fabf7
commit 04045d6dcb
5 changed files with 103 additions and 2 deletions

View File

@@ -36,6 +36,27 @@ The ``--after`` argument allows you to specify whether the chip should be reset
* ``--after no_reset`` leaves the chip in the serial bootloader, no reset is performed.
* ``--after no_reset_stub`` leaves the chip in the stub bootloader, no reset is performed.
Connect Loop
------------
Esptool supports connection loops, where the user can specify how many times to try to open a port. The delay between retries is 0.1 seconds. This can be useful for example when the chip is in deep sleep or esptool was started before the chip was connected to the PC. A connection loop can be created by setting the ``ESPTOOL_OPEN_PORT_ATTEMPTS`` environment variable.
This feature can also be enabled by using the ``open_port_attempts`` configuration option, for more details regarding config options see :ref:`Configuration file <config>` section.
There are 3 possible values for this option:
.. list::
* ``0`` will keep trying to connect to the chip indefinitely
* ``1`` will try to connect to the chip only once (default)
* ``N`` will try to connect to the chip N times
.. note::
This option is only available if both the ``--port`` and ``--chip`` arguments are set.
.. _disable_stub:
Disabling the Stub Loader

View File

@@ -78,7 +78,7 @@ Sample configuration file:
Options
-------
Complete list configurable options:
Complete list of configurable options:
+------------------------------+-----------------------------------------------------------+----------+
| Option | Description | Default |
@@ -107,6 +107,8 @@ Complete list configurable options:
+------------------------------+-----------------------------------------------------------+----------+
| reset_delay | Time to wait before the boot pin is released after reset | 0.05 s |
+------------------------------+-----------------------------------------------------------+----------+
| open_port_attempts | Number of attempts to open the port (0 - infinite) | 1 |
+------------------------------+-----------------------------------------------------------+----------+
| custom_reset_sequence | Custom reset sequence for resetting into the bootloader | |
+------------------------------+-----------------------------------------------------------+----------+

View File

@@ -66,7 +66,13 @@ from esptool.cmds import (
write_mem,
)
from esptool.config import load_config_file
from esptool.loader import DEFAULT_CONNECT_ATTEMPTS, StubFlasher, ESPLoader, list_ports
from esptool.loader import (
DEFAULT_CONNECT_ATTEMPTS,
DEFAULT_OPEN_PORT_ATTEMPTS,
StubFlasher,
ESPLoader,
list_ports,
)
from esptool.targets import CHIP_DEFS, CHIP_LIST, ESP32ROM
from esptool.util import (
FatalError,
@@ -74,6 +80,7 @@ from esptool.util import (
flash_size_bytes,
strip_chip_name,
)
from itertools import chain, cycle, repeat
import serial
@@ -763,6 +770,27 @@ def main(argv=None, esp=None):
print("Found %d serial ports" % len(ser_list))
else:
ser_list = [args.port]
open_port_attempts = os.environ.get(
"ESPTOOL_OPEN_PORT_ATTEMPTS", DEFAULT_OPEN_PORT_ATTEMPTS
)
try:
open_port_attempts = int(open_port_attempts)
except ValueError:
raise SystemExit("Invalid value for ESPTOOL_OPEN_PORT_ATTEMPTS")
if open_port_attempts != 1:
if args.port is None or args.chip == "auto":
print(
"WARNING: The ESPTOOL_OPEN_PORT_ATTEMPTS (open_port_attempts) option can only be used with --port and --chip arguments."
)
else:
esp = esp or connect_loop(
args.port,
initial_baud,
args.chip,
open_port_attempts,
args.trace,
args.before,
)
esp = esp or get_default_connected_device(
ser_list,
port=args.port,
@@ -1092,6 +1120,53 @@ def expand_file_arguments(argv):
return argv
def connect_loop(
port: str,
initial_baud: int,
chip: str,
max_retries: int,
trace: bool = False,
before: str = "default_reset",
):
chip_class = CHIP_DEFS[chip]
esp = None
print(f"Serial port {port}")
first = True
ten_cycle = cycle(chain(repeat(False, 9), (True,)))
retry_loop = chain(
repeat(False, max_retries - 1), (True,) if max_retries else cycle((False,))
)
for last, every_tenth in zip(retry_loop, ten_cycle):
try:
esp = chip_class(port, initial_baud, trace)
if not first:
# break the retrying line
print("")
esp.connect(before)
return esp
except (
FatalError,
serial.serialutil.SerialException,
IOError,
OSError,
) as err:
if esp and esp._port:
esp._port.close()
esp = None
if first:
print(err)
print("Retrying failed connection", end="", flush=True)
first = False
if last:
raise err
if every_tenth:
# print a dot every second
print(".", end="", flush=True)
time.sleep(0.1)
def get_default_connected_device(
serial_list,
port,

View File

@@ -19,6 +19,7 @@ CONFIG_OPTIONS = [
"connect_attempts",
"write_block_attempts",
"reset_delay",
"open_port_attempts",
"custom_reset_sequence",
]

View File

@@ -98,6 +98,8 @@ DEFAULT_SERIAL_WRITE_TIMEOUT = cfg.getfloat("serial_write_timeout", 10)
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)
# Number of times to try opening the serial port
DEFAULT_OPEN_PORT_ATTEMPTS = cfg.getint("open_port_attempts", 1)
def timeout_per_mb(seconds_per_mb, size_bytes):