From c34c83df8f0c8cef7e0ffd1fa9340e5052ff10e0 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 16 Jul 2024 19:22:46 +0200 Subject: [PATCH] feat(esp-idf-monitor): Add --open-port-attempts flag esp-idf-monitor frequently fails when trying to open the serial port of a device which deep-sleeps often: $ idf.py monitor -p /dev/cu.usbmodem6101 --- esp-idf-monitor 1.4.0 on /dev/cu.usbmodem6101 115200 --- --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- [Errno 2] could not open port: [...] No such file or directory: '/dev/cu.usbmodem6101' Connection to /dev/cu.usbmodem6101 failed. Available ports: /dev/cu.usbmodemF554393F21402 This commit adds a new flag `--open-port-attempts` which retries opening the port indefinitely until the device shows up: $ idf.py monitor -p /dev/cu.usbmodem6101 --open-port-attempts 0 --- esp-idf-monitor 1.4.0 on /dev/cu.usbmodem6101 115200 --- --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- [Errno 2] could not open port: [...] No such file or directory: '/dev/cu.usbmodem6101' Retrying to open port ...... ESP-ROM:esp32s3-20210327 Also, add option to reconfigure reopening retry period. This will greatly increase the chance of catching more logs in devices which deep-sleep. Closes https://github.com/espressif/esp-idf-monitor/pull/15 --- esp_idf_monitor/base/argument_parser.py | 10 ++++- esp_idf_monitor/base/constants.py | 7 +++- esp_idf_monitor/base/serial_reader.py | 50 +++++++++++++++---------- esp_idf_monitor/config.py | 3 +- esp_idf_monitor/idf_monitor.py | 9 +++-- 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/esp_idf_monitor/base/argument_parser.py b/esp_idf_monitor/base/argument_parser.py index 0ec4262..47bebfd 100644 --- a/esp_idf_monitor/base/argument_parser.py +++ b/esp_idf_monitor/base/argument_parser.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import argparse @@ -139,4 +139,12 @@ def get_parser(): # type: () -> argparse.ArgumentParser default=False, action='store_true') + parser.add_argument( + '--open-port-attempts', + help='Number of attempts to wait for the port to appear (useful if the device is not connected or in deep sleep).' + 'Use 0 for infinite attempts.', + default=1, + type=int + ) + return parser diff --git a/esp_idf_monitor/base/constants.py b/esp_idf_monitor/base/constants.py index 8d0230a..5ecb51c 100644 --- a/esp_idf_monitor/base/constants.py +++ b/esp_idf_monitor/base/constants.py @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 +from esp_idf_monitor.base.key_config import cfg + # Control-key characters CTRL_C = '\x03' CTRL_H = '\x08' @@ -45,6 +47,7 @@ EVENT_QUEUE_TIMEOUT = 0.03 # timeout before raising queue.Empty exception in case of empty event queue ESPPORT_ENVIRON = str('ESPPORT') +ESPTOOL_OPEN_PORT_ATTEMPTS_ENVIRON = str('ESPTOOL_OPEN_PORT_ATTEMPTS') MAKEFLAGS_ENVIRON = 'MAKEFLAGS' GDB_UART_CONTINUE_COMMAND = '+$c#63' @@ -55,7 +58,7 @@ LAST_LINE_THREAD_INTERVAL = 0.1 MINIMAL_EN_LOW_DELAY = 0.005 -RECONNECT_DELAY = 0.5 # timeout between reconnect tries +RECONNECT_DELAY = cfg.getint('reconnect_delay', 0.5) # timeout between reconnect tries CHECK_ALIVE_FLAG_TIMEOUT = 0.25 # timeout for checking alive flags (currently used by serial reader) # closing wait timeout for serial port diff --git a/esp_idf_monitor/base/serial_reader.py b/esp_idf_monitor/base/serial_reader.py index f3df8bf..bff7a51 100644 --- a/esp_idf_monitor/base/serial_reader.py +++ b/esp_idf_monitor/base/serial_reader.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import queue # noqa: F401 @@ -25,14 +25,15 @@ class SerialReader(Reader): event queue, until stopped. """ - def __init__(self, serial_instance, event_queue, reset, target): - # type: (serial.Serial, queue.Queue, bool, str) -> None + def __init__(self, serial_instance, event_queue, reset, open_port_attempts, target): + # type: (serial.Serial, queue.Queue, bool, int, str) -> None super(SerialReader, self).__init__() self.baud = serial_instance.baudrate self.serial = serial_instance self.event_queue = event_queue self.gdb_exit = False self.reset = reset + self.open_port_attempts = open_port_attempts self.reset_strategy = Reset(serial_instance, target) if not hasattr(self.serial, 'cancel_read'): # enable timeout for checking alive flag, @@ -46,40 +47,51 @@ def run(self): try: # We can come to this thread at startup or from external application line GDB. # If we come from GDB we would like to continue to run without reset. - self.open_serial(reset=not self.gdb_exit and self.reset) + self.reset = not self.gdb_exit and self.reset + self.open_serial(reset=self.reset) + # Successfully connected, so any further reconnections should occur without a reset. + self.reset = False except serial.SerialException as e: print(e) - # if connection to port fails suggest other available ports - port_list = '\n'.join( - [ - p.device - for p in list_ports.comports() - if not p.device.endswith(FILTERED_PORTS) - ] - ) - yellow_print(f'Connection to {self.serial.portstr} failed. Available ports:\n{port_list}') - return + if self.open_port_attempts == 1: + # If the connection to the port fails and --open-port-attempts was not specified, + # recommend other available ports and exit. + port_list = '\n'.join( + [ + p.device + for p in list_ports.comports() + if not p.device.endswith(FILTERED_PORTS) + ] + ) + yellow_print(f'Connection to {self.serial.portstr} failed. Available ports:\n{port_list}') + return self.gdb_exit = False try: while self.alive: try: - data = self.serial.read(self.serial.in_waiting or 1) - except (serial.SerialException, IOError) as e: + if self.serial.is_open: + # in_waiting assumes the port is already open + data = self.serial.read(self.serial.in_waiting or 1) + else: + raise serial.PortNotOpenError + except (serial.SerialException, IOError, OSError) as e: data = b'' # self.serial.open() was successful before, therefore, this is an issue related to # the disappearance of the device - red_print(e.strerror) + red_print(str(e)) yellow_print('Waiting for the device to reconnect', newline='') self.close_serial() while self.alive: # so that exiting monitor works while waiting try: time.sleep(RECONNECT_DELAY) # reset on reconnect can be unexpected for wakeup from deepsleep using JTAG - self.open_serial(reset=False) + self.open_serial(reset=self.reset) + self.reset = False break # device connected - except serial.SerialException: + except (serial.SerialException, IOError, OSError): yellow_print('.', newline='') sys.stderr.flush() + yellow_print('') # go to new line if data: self.event_queue.put((TAG_SERIAL, data), False) diff --git a/esp_idf_monitor/config.py b/esp_idf_monitor/config.py index 89b4af0..e209bd0 100644 --- a/esp_idf_monitor/config.py +++ b/esp_idf_monitor/config.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD, +# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD, # other contributors as noted. # # SPDX-License-Identifier: Apache-2.0 @@ -20,6 +20,7 @@ 'chip_reset_bootloader_key', 'exit_menu_key', 'skip_menu_key', + 'reconnect_delay', 'custom_reset_sequence', # from esptool config ] diff --git a/esp_idf_monitor/idf_monitor.py b/esp_idf_monitor/idf_monitor.py index 0a8f2f7..a10c5f8 100644 --- a/esp_idf_monitor/idf_monitor.py +++ b/esp_idf_monitor/idf_monitor.py @@ -9,7 +9,7 @@ # - If core dump output is detected, it is converted to a human-readable report # by espcoredump.py. # -# SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 # # Contains elements taken from miniterm "Very simple serial terminal" which @@ -44,6 +44,7 @@ DEFAULT_TARGET_RESET, DEFAULT_TOOLCHAIN_PREFIX, ESPPORT_ENVIRON, + ESPTOOL_OPEN_PORT_ATTEMPTS_ENVIRON, EVENT_QUEUE_TIMEOUT, FILTERED_PORTS, GDB_EXIT_TIMEOUT, GDB_UART_CONTINUE_COMMAND, @@ -90,6 +91,7 @@ def __init__( make='make', # type: str encrypted=False, # type: bool reset=DEFAULT_TARGET_RESET, # type: bool + open_port_attempts=1, # type: int toolchain_prefix=DEFAULT_TOOLCHAIN_PREFIX, # type: str eol='CRLF', # type: str decode_coredumps=COREDUMP_DECODE_INFO, # type: str @@ -128,7 +130,7 @@ def __init__( # testing hook: when running tests, input from console is ignored socket_test_mode = os.environ.get('ESP_IDF_MONITOR_TEST') == '1' self.serial = serial_instance - self.serial_reader = SerialReader(self.serial, self.event_queue, reset, target) # type: Reader + self.serial_reader = SerialReader(self.serial, self.event_queue, reset, open_port_attempts, target) # type: Reader self.gdb_helper = GDBHelper(toolchain_prefix, websocket_client, self.elf_file, self.serial.port, self.serial.baudrate) if self.elf_exists else None @@ -403,7 +405,7 @@ def main() -> None: # has a check for this). # To make sure the key as well as the value are str type, by the requirements of subprocess espport_val = str(args.port) - os.environ.update({ESPPORT_ENVIRON: espport_val}) + os.environ.update({ESPPORT_ENVIRON: espport_val, ESPTOOL_OPEN_PORT_ATTEMPTS_ENVIRON: str(args.open_port_attempts)}) cls = SerialMonitor yellow_print('--- esp-idf-monitor {v} on {p.name} {p.baudrate} ---'.format(v=__version__, p=serial_instance)) @@ -414,6 +416,7 @@ def main() -> None: args.make, args.encrypted, not args.no_reset, + args.open_port_attempts, args.toolchain_prefix, args.eol, args.decode_coredumps,