From 3938ccfc6e03bbc24e52aa40a1fb30a506729766 Mon Sep 17 00:00:00 2001 From: Zapta Date: Mon, 14 Oct 2024 00:24:02 -0700 Subject: [PATCH] Updated the scons pipe filter of the icrprog program. 1. Added a range detector. The filter is now active only within the range of the iceprog output lines. 2. Changed the line erasure model. We track with a flag if the previous line should be erased before printing the current line (for the percents progress indicator). This change was tested with Alhambra II. The Fumo and Tinyprog filters should be changed in the same way but I currently don't have boards to test to leaving as is. --- apio/managers/scons_filter.py | 139 ++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 49 deletions(-) diff --git a/apio/managers/scons_filter.py b/apio/managers/scons_filter.py index d33a3989..eab3a7ab 100644 --- a/apio/managers/scons_filter.py +++ b/apio/managers/scons_filter.py @@ -42,15 +42,15 @@ class RangeEvents(Enum): END_AFTER = 4 # Range ends, after the current line. -class SectionDetector: - """Base classifier of a range of lines within the sequence of stdout/err +class RangeDetector: + """Base detector of a range of lines within the sequence of stdout/err lines recieves from the scons subprocess.""" def __init__(self): self._in_range = False def update(self, pipe_id: PipeId, line: str) -> bool: - """Updates the section classifier with the next stdout/err line. + """Updates the range detector with the next stdout/err line. return True iff detector classified this line to be within a range.""" prev_state = self._in_range @@ -85,11 +85,12 @@ def classify_line( raise NotImplementedError("Should be implemented by a subclass") -class PnrSectionDetector(SectionDetector): - """Implements a RangeDetector for the nextpnr command verbose log lines.""" +class PnrRangeDetector(RangeDetector): + """Implements a RangeDetector for the nextpnr command verbose + log lines.""" def classify_line(self, pipe_id: PipeId, line: str) -> RangeEvents: - # -- Brek line into words. + # -- Break line into words. tokens = line.split() # -- Range start: A nextpnr command on stdout without @@ -108,6 +109,29 @@ def classify_line(self, pipe_id: PipeId, line: str) -> RangeEvents: return None +class IceProgRangeDetector(RangeDetector): + """Implements a RangeDetector for the iceprog command output.""" + + def __init__(self): + super().__init__() + # -- Indicates if the last line should be erased before printing the + # -- next one. This happens with interactive progress meters. + self.pending_erasure = False + + def classify_line(self, pipe_id: PipeId, line: str) -> RangeEvents: + # -- Range start: A nextpnr command on stdout without + # -- the -q (quiet) flag. + if pipe_id == PipeId.STDOUT and line.startswith("iceprog"): + self.pending_erasure = False + return RangeEvents.START_AFTER + + # Range end: The end message of nextnpr. + if pipe_id == PipeId.STDERR and line.startswith("Bye."): + return RangeEvents.END_AFTER + + return None + + class SconsFilter: """Implements the filtering and printing of the stdout/err streams of the scons subprocess. Accepts a line one at a time, detects lines ranges of @@ -115,7 +139,8 @@ class SconsFilter: stdout.""" def __init__(self): - self._pnr_detector = PnrSectionDetector() + self._pnr_detector = PnrRangeDetector() + self._iceprog_detector = IceProgRangeDetector() def on_stdout_line(self, line: str) -> None: """Stdout pipe calls this on each line.""" @@ -150,8 +175,9 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: from other programs. See the PNR detector for an example. """ - # -- Update the classifiers + # -- Update the range detectors. in_pnr_verbose_range = self._pnr_detector.update(pipe_id, line) + in_iceprog_range = self._iceprog_detector.update(pipe_id, line) # -- Handle the line while in the nextpnr verbose log range. if pipe_id == PipeId.STDERR and in_pnr_verbose_range: @@ -172,6 +198,52 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: click.secho(f"{line}", fg=line_color) return + # -- Special handling for iceprog line range. + if pipe_id == PipeId.STDERR and in_iceprog_range: + # -- Iceprog prints blank likes that are used as line erasers. + # -- We don't need them here. + if len(line) == 0: + return + + # -- If the last iceprog line was a to-be-erased line, erase it + # -- now and clear the flag. + if self._iceprog_detector.pending_erasure: + print( + CURSOR_UP + ERASE_LINE, + end="", + flush=True, + ) + self._iceprog_detector.pending_erasure = False + + # -- Determine if the current line should be erased before we will + # -- print the next line. + # -- + # -- Match outputs like these "addr 0x001400 3%" + # -- Regular expression remainder: + # -- ^ --> Match the begining of the line + # -- \s --> Match one blank space + # -- [0-9A-F]+ one or more hexadecimal digit + # -- \d{1,2} one or two decimal digits + pattern = r"^addr\s0x[0-9A-F]+\s+\d{1,2}%" + + # -- Calculate if there is a match! + match = re.search(pattern, line) + + # -- If the line is to be erased set the flag. + if match: + self._iceprog_detector.pending_erasure = True + + # -- Determine line color by its content and print it. + line_color = self._assign_line_color( + line, + [ + (r"^done.", "green"), + (r"^VERIFY OK", "green"), + ], + ) + click.secho(line, fg=line_color) + return + # -- Special handling for Fumo lines. if pipe_id == PipeId.STDOUT: pattern_fomu = r"^Download\s*\[=*" @@ -179,12 +251,11 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: if match: # -- Delete the previous line # - # -- TODO: Since the commit listed below we preserve blank - # -- stdour/err lines. Iceprog emits an empty line after each - # -- percentage line so we changed it below to erase two lines. - # -- If this is also the case with tinyprog, apply the same - # -- change here. Delete this TODO when resolved. - # - Comit 93fc9bc4f3bfd21568e2d66f11976831467e3b97. + # -- NOTE: If the progress line will scroll instead of + # -- overwriting each other, try to add erasure of a second + # -- line. This is due to the commit below which restored + # -- empty lines. + # - Commit 93fc9bc4f3bfd21568e2d66f11976831467e3b97. # print(CURSOR_UP + ERASE_LINE, end="", flush=True) click.secho(line, fg="green") @@ -208,46 +279,16 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: if match_tinyprog and " 0%|" not in line: # -- Delete the previous line. # - # -- TODO: Since the commit listed below we preserve blank - # -- stdour/err lines. Iceprog emits an empty line after each - # -- percentage line so we changed it below to erase two lines. - # -- If this is also the case with tinyprog, apply the same - # -- change here. Delete this TODO when resolved. - # - Comit 93fc9bc4f3bfd21568e2d66f11976831467e3b97. + # -- NOTE: If the progress line will scroll instead of + # -- overwriting each other, try to add erasure of a second + # -- line. This is due to the commit below which restored + # -- empty lines. + # - Commit 93fc9bc4f3bfd21568e2d66f11976831467e3b97. # print(CURSOR_UP + ERASE_LINE, end="", flush=True) click.secho(line) return - # -- Special handling for iceprog lines. - if pipe_id == PipeId.STDERR: - # -- Match outputs like these "addr 0x001400 3%" - # -- Regular expression remainder: - # -- ^ --> Match the begining of the line - # -- \s --> Match one blank space - # -- [0-9A-F]+ one or more hexadecimal digit - # -- \d{1,2} one or two decimal digits - pattern = r"^addr\s0x[0-9A-F]+\s+\d{1,2}%" - - # -- Calculate if there is a match! - match = re.search(pattern, line) - - # -- It is a match! (iceprog is running!) - # -- (or if it is the end of the writing!) - # -- (or if it is the end of verifying!) - completed_ok = "done." in line or "VERIFY OK" in line - if match or completed_ok: - # -- Delete the previous two lines. We erase two lines because - # -- iceprog emits an empty line after each percentage line. - print( - CURSOR_UP + ERASE_LINE + CURSOR_UP + ERASE_LINE, - end="", - flush=True, - ) - line_color = "green" if completed_ok else None - click.secho(line, fg=line_color) - return - # Handling the rest of the stdout lines. if pipe_id == PipeId.STDOUT: # Default stdout line coloring.