Skip to content

Commit

Permalink
feat(esptool): Add --retry-open-serial flag, config file entry and envar
Browse files Browse the repository at this point in the history
`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 flag `--retry-open-serial` (with corresponding env variable and cfg file entry)
which retries opening the port indefinitely until the device shows up:

$ esptool.py --chip esp32s3 -p /dev/cu.usbmodem6101 [...] write_flash --retry-open-serial 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)
[...]
  • Loading branch information
2opremio committed Jul 17, 2024
1 parent 2b0ec7a commit ab92703
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 45 deletions.
2 changes: 2 additions & 0 deletions docs/en/esptool/configuration-file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ Complete list configurable options:
+------------------------------+-----------------------------------------------------------+----------+
| custom_reset_sequence | Custom reset sequence for resetting into the bootloader | |
+------------------------------+-----------------------------------------------------------+----------+
| retry_open_serial | Retry opening the serial port indefinitely | False |
+------------------------------+-----------------------------------------------------------+----------+

Custom Reset Sequence
---------------------
Expand Down
21 changes: 19 additions & 2 deletions esptool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@
write_mem,
)
from esptool.config import load_config_file
from esptool.loader import DEFAULT_CONNECT_ATTEMPTS, ESPLoader, list_ports
from esptool.loader import (
DEFAULT_CONNECT_ATTEMPTS,
DEFAULT_RETRY_OPEN_SERIAL,
ESPLoader,
list_ports,
)
from esptool.targets import CHIP_DEFS, CHIP_LIST, ESP32ROM
from esptool.util import (
FatalError,
Expand Down Expand Up @@ -169,6 +174,16 @@ def main(argv=None, esp=None):
default=os.environ.get("ESPTOOL_CONNECT_ATTEMPTS", DEFAULT_CONNECT_ATTEMPTS),
)

parser.add_argument(
"--retry-open-serial",
help=(
"Retry opening the serial port indefinitely. "
"Default: %s" % DEFAULT_RETRY_OPEN_SERIAL
),
default=os.environ.get("ESPTOOL_RETRY_OPEN_SERIAL", DEFAULT_RETRY_OPEN_SERIAL),
action="store_true",
)

subparsers = parser.add_subparsers(
dest="operation", help="Run esptool.py {command} -h for additional help"
)
Expand Down Expand Up @@ -723,6 +738,7 @@ def add_spi_flash_subparsers(
port=args.port,
connect_attempts=args.connect_attempts,
initial_baud=initial_baud,
retry_open_serial=args.retry_open_serial,
chip=args.chip,
trace=args.trace,
before=args.before,
Expand Down Expand Up @@ -1044,6 +1060,7 @@ def get_default_connected_device(
port,
connect_attempts,
initial_baud,
retry_open_serial=False,
chip="auto",
trace=False,
before="default_reset",
Expand All @@ -1058,7 +1075,7 @@ def get_default_connected_device(
)
else:
chip_class = CHIP_DEFS[chip]
_esp = chip_class(each_port, initial_baud, trace)
_esp = chip_class(each_port, initial_baud, trace, retry_open_serial)
_esp.connect(before, connect_attempts)
break
except (FatalError, OSError) as err:
Expand Down
1 change: 1 addition & 0 deletions esptool/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"write_block_attempts",
"reset_delay",
"custom_reset_sequence",
"retry_open_serial",
]


Expand Down
114 changes: 71 additions & 43 deletions esptool/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
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)
# Default number of times to try connection
DEFAULT_RETRY_OPEN_SERIAL = cfg.getboolean("retry_open_serial", False)
# Number of times to try writing a data block
WRITE_BLOCK_ATTEMPTS = cfg.getint("write_block_attempts", 3)

Expand Down Expand Up @@ -192,6 +194,7 @@ class ESPLoader(object):
BOOTLOADER_IMAGE: Optional[object] = None

DEFAULT_PORT = "/dev/ttyUSB0"
DEFAULT_RETRY_OPEN_SERIAL = False

USES_RFC2217 = False

Expand Down Expand Up @@ -281,7 +284,13 @@ class ESPLoader(object):
# Number of attempts to write flash data
WRITE_FLASH_ATTEMPTS = 2

def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False):
def __init__(
self,
port=DEFAULT_PORT,
baud=ESP_ROM_BAUD,
trace_enabled=False,
retry_open_serial=DEFAULT_RETRY_OPEN_SERIAL,
):
"""Base constructor for ESPLoader bootloader interaction
Don't call this constructor, either instantiate a specific
Expand All @@ -308,51 +317,70 @@ def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False):
}

if isinstance(port, str):
try:
self._port = serial.serial_for_url(
port, exclusive=True, do_not_open=True
)
if sys.platform == "win32":
# When opening a port on Windows,
# the RTS/DTR (active low) lines
# need to be set to False (pulled high)
# to avoid unwanted chip reset
self._port.rts = False
self._port.dtr = False
self._port.open()
except serial.serialutil.SerialException as e:
port_issues = [
[ # does not exist error
re.compile(r"Errno 2|FileNotFoundError", re.IGNORECASE),
"Check if the port is correct and ESP connected",
],
[ # busy port error
re.compile(r"Access is denied", re.IGNORECASE),
"Check if the port is not used by another task",
],
]
if sys.platform.startswith("linux"):
port_issues.append(
[ # permission denied error
re.compile(r"Permission denied", re.IGNORECASE),
(
"Try to add user into dialout group: "
"sudo usermod -a -G dialout $USER"
),
],
printed_failure = False
retry_attempts = 0
while True:
try:
self._port = serial.serial_for_url(
port, exclusive=True, do_not_open=True
)
if sys.platform == "win32":
# When opening a port on Windows,
# the RTS/DTR (active low) lines
# need to be set to False (pulled high)
# to avoid unwanted chip reset
self._port.rts = False
self._port.dtr = False
self._port.open()
if retry_attempts > 0:
# break the retrying line
print("")
break
except serial.serialutil.SerialException as e:
if retry_open_serial:
if not printed_failure:
print(e)
print("Retrying to open port ", end="")
printed_failure = True
else:
if retry_attempts % 9 == 0:
# print a dot every second
print(".", end="")
time.sleep(0.1)
retry_attempts += 1
continue
port_issues = [
[ # does not exist error
re.compile(r"Errno 2|FileNotFoundError", re.IGNORECASE),
"Check if the port is correct and ESP connected",
],
[ # busy port error
re.compile(r"Access is denied", re.IGNORECASE),
"Check if the port is not used by another task",
],
]
if sys.platform.startswith("linux"):
port_issues.append(
[ # permission denied error
re.compile(r"Permission denied", re.IGNORECASE),
(
"Try to add user into dialout group: "
"sudo usermod -a -G dialout $USER"
),
],
)

hint_msg = ""
for port_issue in port_issues:
if port_issue[0].search(str(e)):
hint_msg = f"\nHint: {port_issue[1]}\n"
break
hint_msg = ""
for port_issue in port_issues:
if port_issue[0].search(str(e)):
hint_msg = f"\nHint: {port_issue[1]}\n"
break

raise FatalError(
f"Could not open {port}, the port is busy or doesn't exist."
f"\n({e})\n"
f"{hint_msg}"
)
raise FatalError(
f"Could not open {port}, the port is busy or doesn't exist."
f"\n({e})\n"
f"{hint_msg}"
)
else:
self._port = port
self._slip_reader = slip_reader(self._port, self.trace)
Expand Down

0 comments on commit ab92703

Please sign in to comment.