diff --git a/docs/en/api-guides/tools/idf-monitor.rst b/docs/en/api-guides/tools/idf-monitor.rst index 7ae6c57ce0..90a23d1bc2 100644 --- a/docs/en/api-guides/tools/idf-monitor.rst +++ b/docs/en/api-guides/tools/idf-monitor.rst @@ -36,6 +36,8 @@ For easy interaction with IDF Monitor, use the keyboard shortcuts given in the t +-------------------+--------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | - Ctrl+Y | Stop/resume log output printing on screen | Discards all incoming serial data while activated. Allows to quickly pause and examine log output without quitting the monitor. | +-------------------+--------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| - Ctrl+L | Stop/resume log output saved to file | Creates a file in the project directory and the output is written to that file until this is disabled with the same keyboard shortcut (or IDF Monitor exits). | ++-------------------+--------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | - Ctrl+H | Display all keyboard shortcuts | | +-------------------+--------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/tools/idf_monitor.py b/tools/idf_monitor.py index 8ff925add4..f5c5918c46 100755 --- a/tools/idf_monitor.py +++ b/tools/idf_monitor.py @@ -35,6 +35,7 @@ from builtins import bytes import subprocess import argparse import codecs +import datetime import re import os try: @@ -50,6 +51,7 @@ import threading import ctypes import types from distutils.version import StrictVersion +from io import open key_description = miniterm.key_description @@ -62,6 +64,7 @@ CTRL_R = '\x12' CTRL_T = '\x14' CTRL_Y = '\x19' CTRL_P = '\x10' +CTRL_L = '\x0c' CTRL_RBRACKET = '\x1d' # Ctrl+] # ANSI terminal codes (if changed, regular expressions in LineMatcher need to be udpated) @@ -355,6 +358,7 @@ class Monitor(object): self._force_line_print = False self._output_enabled = True self._serial_check_exit = socket_mode + self._log_file = None def invoke_processing_last_line(self): self.event_queue.put((TAG_SERIAL_FLUSH, b''), False) @@ -388,6 +392,7 @@ class Monitor(object): try: self.console_reader.stop() self.serial_reader.stop() + self.stop_logging() # Cancelling _invoke_processing_last_line_timer is not # important here because receiving empty data doesn't matter. self._invoke_processing_last_line_timer = None @@ -426,8 +431,8 @@ class Monitor(object): if line != b"": if self._serial_check_exit and line == self.exit_key.encode('latin-1'): raise SerialStopException() - if self._output_enabled and (self._force_line_print or self._line_matcher.match(line.decode(errors="ignore"))): - self.console.write_bytes(line + b'\n') + if self._force_line_print or self._line_matcher.match(line.decode(errors="ignore")): + self._print(line + b'\n') self.handle_possible_pc_address_in_line(line) self.check_gdbstub_trigger(line) self._force_line_print = False @@ -438,9 +443,8 @@ class Monitor(object): if self._last_line_part != b"": if self._force_line_print or (finalize_line and self._line_matcher.match(self._last_line_part.decode(errors="ignore"))): self._force_line_print = True - if self._output_enabled: - self.console.write_bytes(self._last_line_part) - self.handle_possible_pc_address_in_line(self._last_line_part) + self._print(self._last_line_part) + self.handle_possible_pc_address_in_line(self._last_line_part) self.check_gdbstub_trigger(self._last_line_part) # It is possible that the incomplete line cuts in half the PC # address. A small buffer is kept and will be used the next time @@ -477,6 +481,8 @@ class Monitor(object): self.run_make("app-flash") elif c == CTRL_Y: # Toggle output display self.output_toggle() + elif c == CTRL_L: # Toggle saving output into file + self.toggle_logging() elif c == CTRL_P: yellow_print("Pause app (enter bootloader mode), press Ctrl-T Ctrl-R to restart") # to fast trigger pause without press menu key @@ -504,6 +510,7 @@ class Monitor(object): --- {makecmd:7} Build & flash project --- {appmake:7} Build & flash app only --- {output:7} Toggle output display +--- {log:7} Toggle saving output into file --- {pause:7} Reset target into bootloader to pause app via RTS line """.format(version=__version__, exit=key_description(self.exit_key), @@ -512,6 +519,7 @@ class Monitor(object): makecmd=key_description(CTRL_F), appmake=key_description(CTRL_A), output=key_description(CTRL_Y), + log=key_description(CTRL_L), pause=key_description(CTRL_P)) def __enter__(self): @@ -570,7 +578,7 @@ class Monitor(object): try: translation = subprocess.check_output(cmd, cwd=".") if b"?? ??:0" not in translation: - yellow_print(translation.decode()) + self._print(translation.decode(), console_printer=yellow_print) except OSError as e: red_print("%s: %s" % (" ".join(cmd), e)) @@ -624,6 +632,48 @@ class Monitor(object): self._output_enabled = not self._output_enabled yellow_print("\nToggle output display: {}, Type Ctrl-T Ctrl-Y to show/disable output again.".format(self._output_enabled)) + def toggle_logging(self): + if self._log_file: + self.stop_logging() + else: + self.start_logging() + + def start_logging(self): + if not self._log_file: + try: + name = "log.{}.{}.txt".format(os.path.splitext(os.path.basename(self.elf_file))[0], + datetime.datetime.now().strftime('%Y%m%d%H%M%S')) + self._log_file = open(name, "wb+") + yellow_print("\nLogging is enabled into file {}".format(name)) + except Exception as e: + red_print("\nLog file {} cannot be created: {}".format(name, e)) + + def stop_logging(self): + if self._log_file: + try: + name = self._log_file.name + self._log_file.close() + yellow_print("\nLogging is disabled and file {} has been closed".format(name)) + except Exception as e: + red_print("\nLog file cannot be closed: {}".format(e)) + finally: + self._log_file = None + + def _print(self, string, console_printer=None): + if console_printer is None: + console_printer = self.console.write_bytes + if self._output_enabled: + console_printer(string) + if self._log_file: + try: + if isinstance(string, type(u'')): + string = string.encode() + self._log_file.write(string) + except Exception as e: + red_print("\nCannot write to file: {}".format(e)) + # don't fill-up the screen with the previous errors (probably consequent prints would fail also) + self.stop_logging() + def main(): parser = argparse.ArgumentParser("idf_monitor - a serial output monitor for esp-idf")