From ab927033e19b3f5d6b3cb9e79f404c2f0ff5b3b0 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Wed, 17 Jul 2024 01:27:21 +0200 Subject: [PATCH] feat(esptool): Add --retry-open-serial flag, config file entry and envar `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) [...] --- docs/en/esptool/configuration-file.rst | 2 + esptool/__init__.py | 21 ++++- esptool/config.py | 1 + esptool/loader.py | 114 +++++++++++++++---------- 4 files changed, 93 insertions(+), 45 deletions(-) diff --git a/docs/en/esptool/configuration-file.rst b/docs/en/esptool/configuration-file.rst index c538c608a..81ddcfa4d 100644 --- a/docs/en/esptool/configuration-file.rst +++ b/docs/en/esptool/configuration-file.rst @@ -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 --------------------- diff --git a/esptool/__init__.py b/esptool/__init__.py index b66a2f1b0..3b23f2738 100644 --- a/esptool/__init__.py +++ b/esptool/__init__.py @@ -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, @@ -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" ) @@ -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, @@ -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", @@ -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: diff --git a/esptool/config.py b/esptool/config.py index 5566becca..56f27d5e3 100644 --- a/esptool/config.py +++ b/esptool/config.py @@ -20,6 +20,7 @@ "write_block_attempts", "reset_delay", "custom_reset_sequence", + "retry_open_serial", ] diff --git a/esptool/loader.py b/esptool/loader.py index 8cfc3a43e..5f2ff60a1 100644 --- a/esptool/loader.py +++ b/esptool/loader.py @@ -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) @@ -192,6 +194,7 @@ class ESPLoader(object): BOOTLOADER_IMAGE: Optional[object] = None DEFAULT_PORT = "/dev/ttyUSB0" + DEFAULT_RETRY_OPEN_SERIAL = False USES_RFC2217 = False @@ -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 @@ -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)