diff --git a/README.md b/README.md index 8147729..50111ce 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Optional arguments include: - `-S | --save SAVENAME` Saves current query for future reuse. - `-R | --reload SAVENAME` Loads a saved query. Requires query name as string. - `--btable` Print the wire value table at breakpoints to `stdout` (`-b` is required). +- `-p` User parameter for starting time window, ending time window and visible wires to generate waveform *Note: For more detailed information on the query language, check out [syntax.md](syntax.md) diff --git a/sootty/SoottyConfig.py b/sootty/SoottyConfig.py new file mode 100644 index 0000000..9bec47b --- /dev/null +++ b/sootty/SoottyConfig.py @@ -0,0 +1,20 @@ +class SoottyConfig: + def __init__(self, user_start=None, user_end=None, visible_wires=None): + self.user_start = user_start + self.user_end = user_end + self.visible_wires = visible_wires if visible_wires is not None else [] + + def set_user_start(self, user_start): + self.user_start = user_start + + def set_user_end(self, user_end): + self.user_end = user_end + + def set_visible_wires(self, visible_wires): + self.visible_wires = visible_wires + + def get_time_window(self): + return self.user_start, self.user_end + + def get_visible_wires(self): + return self.visible_wires diff --git a/sootty/__main__.py b/sootty/__main__.py index aa6ffae..de8fcab 100644 --- a/sootty/__main__.py +++ b/sootty/__main__.py @@ -5,6 +5,8 @@ from .save import save_query, reload_query from .storage import WireTrace from .visualizer import Visualizer +from .SoottyConfig import SoottyConfig +from .parser import parameter_query def parse_args(): @@ -96,6 +98,9 @@ def parse_args(): help="Loads a saved query. Requires query name as string.", ) + parser.add_argument('-p', '--parameters', help='Specify parameter query') + + args = parser.parse_args() if args.save is not None and args.reload is not None: raise SoottyError( @@ -118,11 +123,12 @@ def parse_args(): args.end, args.output, args.radix, + args.parameters ) def main(): - filename, wires, breakpoints, btable, length, start, end, output, radix = parse_args() + filename, wires, breakpoints, btable, length, start, end, output, radix, parameters = parse_args() if filename is None: raise SoottyError("Input file is required. See --help for more info.") @@ -152,8 +158,14 @@ def main(): if breakpoints is not None: breakpoints = wiretrace.evaluate(breakpoints) + # Create SoottyConfig instance and set user-defined time window + config = SoottyConfig(user_start=None, user_end=None, visible_wires=None) + + if parameters: + config = parameter_query(parameters, config) + # Convert wiretrace to graphical vector image. - image = Visualizer().to_svg( + image = Visualizer(config=config).to_svg( wiretrace, start=start, length=length, diff --git a/sootty/parser.py b/sootty/parser.py index e85aa03..d8de7c7 100644 --- a/sootty/parser.py +++ b/sootty/parser.py @@ -82,3 +82,19 @@ def parse_list(self, expressions: str): parser = ExpressionParser() # initialize global parser object + +def parameter_query(parameter_query, config): + key_values = parameter_query.replace('\n', '').split(':') + + for key_value in key_values: + if key_value[0][0] == 's': #start= 0 1 2 3 4 5 + config.set_user_start(int(key_value[6:len(key_value)])) + elif key_value[0][0] == 'e': #end= 0 1 2 3 + config.set_user_end(int(key_value[4:len(key_value)])) + elif key_value[0][0] == 'v': #visible_wires 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + val = key_value[14:len(key_value)] + config.set_visible_wires(val.split(',')) + + return config + + diff --git a/sootty/visualizer.py b/sootty/visualizer.py index 30ded29..cda8b75 100644 --- a/sootty/visualizer.py +++ b/sootty/visualizer.py @@ -66,9 +66,10 @@ class Debug(Default): class Visualizer: """Converter for wiretrace objects to a svg vector image format.""" - def __init__(self, style=Style.Default): + def __init__(self, style=Style.Default, config=None): """Optionally pass in a style class to control how the visualizer looks.""" self.style = style + self.config = config or SoottyConfig() def to_svg( self, @@ -105,15 +106,24 @@ def to_svg( def _wiretrace_to_svg( self, wiretrace, start, length, wires=None, breakpoints=None, vector_radix=10 ): + user_start, user_end = self.config.get_time_window() + visible_wires = self.config.get_visible_wires() + width = ( 2 * self.style.LEFT_MARGIN + self.style.TEXT_WIDTH + self.style.FULL_WIDTH ) + # Check if 'wires' is not None and not an integer + if wires is not None and not isinstance(wires, int): + num_wires = len(wires) + else: + num_wires = wiretrace.num_wires() + height = ( 2 * self.style.TOP_MARGIN - self.style.WIRE_MARGIN + (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN) - * (1 + (len(wires) if wires else wiretrace.num_wires())) + * (1 + (num_wires if wires else wiretrace.num_wires())) ) svg = ( @@ -121,11 +131,12 @@ def _wiretrace_to_svg( f'' ) + # Pass user_start and user_end to relevant functions svg += self._timestamps_to_svg( left=self.style.LEFT_MARGIN + self.style.TEXT_WIDTH, top=self.style.TOP_MARGIN, start=start, - length=length, + length=length ) if not breakpoints: @@ -136,7 +147,7 @@ def _wiretrace_to_svg( top=self.style.TOP_MARGIN, start=start, length=length, - height=height - 2 * self.style.TOP_MARGIN + self.style.WIRE_MARGIN, + height=height - 2 * self.style.TOP_MARGIN + self.style.WIRE_MARGIN ) # Add the root wiregroup to the image. @@ -151,89 +162,101 @@ def _wiretrace_to_svg( ) svg += result[0] index = result[1] - + # Add each composite wire to the image. if wires is not None: for wire in wires: - svg += self._wire_to_svg( - wiretrace.compute_wire(wire), - left=self.style.LEFT_MARGIN, - top=self.style.TOP_MARGIN - + self.style.WIRE_HEIGHT - + self.style.WIRE_MARGIN - + (index * (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN)), - start=start, - length=length, - ) - index += 1 + # Check if the wire is in the visible_wires list + if wire in visible_wires: + svg += self._wire_to_svg( + wiretrace.compute_wire(wire), + left=self.style.LEFT_MARGIN, + top=self.style.TOP_MARGIN + + self.style.WIRE_HEIGHT + + self.style.WIRE_MARGIN + + (index * (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN)), + start=start, + length=length, + ) + index += 1 wires.clear() # TODO: fix temporary solution for catching exceptions - + svg += "" return svg + def _timestamps_to_svg(self, left, top, start, length): - svg = "" - for index in range(start, start + length, ((length - 1) // 32) + 1): - svg += self._shape_to_svg( - { - "name": "text", - "x": left - + (index - start + 1 / 2) * (self.style.FULL_WIDTH / length), - "y": top + (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN) / 2, - "class": "small", - "fill": self.style.TEXT_COLOR, - "text-anchor": "middle", - "font-family": "monospace", - "content": index, - } - ) - return svg + user_start, user_end = self.config.get_time_window() - def _breakpoints_to_svg(self, breakpoints, left, top, start, length, height): - """Convert a list of breakpoint times to highlights on the svg.""" svg = "" - for i, index in enumerate(breakpoints): - if index >= start and index < start + length: + for index in range(start, start + length): + if (user_start is None or index >= user_start) and (user_end is None or index <= user_end): svg += self._shape_to_svg( { - "name": "rect", + "name": "text", "x": left - + (index - start) * (self.style.FULL_WIDTH / length) - + self.style.TRANS_START, - "y": top, - "width": (self.style.FULL_WIDTH / length), - "height": height, - "fill": self.style.BREAKPOINT_COLOR_LIST[i % len(self.style.BREAKPOINT_COLOR_LIST)], - "fill-opacity": 0.4, + + (index - start + 1 / 2) * (self.style.FULL_WIDTH / length), + "y": top + (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN) / 2, + "class": "small", + "fill": self.style.TEXT_COLOR, + "text-anchor": "middle", + "font-family": "monospace", + "content": index, } ) return svg + + def _breakpoints_to_svg(self, breakpoints, left, top, start, length, height): + user_start, user_end = self.config.get_time_window() + """Convert a list of breakpoint times to highlights on the svg.""" + svg = "" + for i, index in enumerate(breakpoints): + if (user_start is None or index >= user_start) and (user_end is None or index <= user_end): + if index >= start and index < start + length: + svg += self._shape_to_svg( + { + "name": "rect", + "x": left + (index - start) * (self.style.FULL_WIDTH / length) + self.style.TRANS_START, + "y": top, + "width": (self.style.FULL_WIDTH / length), + "height": height, + "fill": self.style.BREAKPOINT_COLOR_LIST[i % len(self.style.BREAKPOINT_COLOR_LIST)], + "fill-opacity": 0.4, + } + ) + return svg + def _wiregroup_to_svg( self, wiregroup, left, top, start, length, wires=None, vector_radix=10 ): + user_start, user_end = self.config.get_time_window() + visible_wires = self.config.get_visible_wires() + svg = "" index = 0 for wire in wiregroup.wires: - if wires == None or wire.name in wires: - if wires: # ensure only one copy of the wire is included - wires.remove(wire.name) - svg += self._wire_to_svg( - wire, - left=left, - top=top - + (index * (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN)), - start=start, - length=length, - vector_radix=vector_radix, - ) - index += 1 + if wires is None or wire.name in wires: + # Check if the wire is in the visible_wires list + if wires is None or wire.name in visible_wires: + if wires: # ensure only one copy of the wire is included + wires.remove(wire.name) + svg += self._wire_to_svg( + wire, + left=left, + top=top + + (index * (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN)), + start=start, + length=length, + vector_radix=vector_radix, + ) + index += 1 # recursively call function on nested wiregroups for group in wiregroup.groups: result = self._wiregroup_to_svg( group, - left=left, - top=top + (index * (self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN)), + left=self.style.LEFT_MARGIN, + top=self.style.TOP_MARGIN + self.style.WIRE_HEIGHT + self.style.WIRE_MARGIN, start=start, length=length, wires=wires, @@ -244,35 +267,73 @@ def _wiregroup_to_svg( return svg, index def _wire_to_svg(self, wire, left, top, start, length, vector_radix=10): - svg = self._shape_to_svg( - { - "name": "text", - "x": left, - "y": top + 15, - "class": "small", - "fill": self.style.TEXT_COLOR, - "font-family": "monospace", - "content": html.escape( - wire.name - if len(wire.name) <= self.style.LABEL_WIDTH - else wire.name[: max(self.style.LABEL_WIDTH - 3, 0)] + "..." - ), - } - ) - for index in range(start, start + length): - svg += self._value_to_svg( - prev=wire[index - 1] if index > 0 else wire[index], - value=wire[index], - width=wire.width(), - left=left - + ((index - start) * (self.style.FULL_WIDTH / length)) - + self.style.TEXT_WIDTH, - top=top, - length=length, - initial=(index == start), - vector_radix=vector_radix, - ) - return svg + user_start, user_end = self.config.get_time_window() + visible_wires = self.config.get_visible_wires() + + # Initialize svg + svg = "" + + # Check if visible_wires is None or empty + if visible_wires is None or not visible_wires: + # Show all wires + svg = self._shape_to_svg( + { + "name": "text", + "x": left, + "y": top + 15, + "class": "small", + "fill": self.style.TEXT_COLOR, + "font-family": "monospace", + "content": html.escape( + wire.name if len(wire.name) <= self.style.LABEL_WIDTH else wire.name[: max(self.style.LABEL_WIDTH - 3, 0)] + "...", + ), + } + ) + for index in range(start, start + length): + if (user_start is None or index >= user_start) and (user_end is None or index <= user_end): + # Show waveform for all wires + svg += self._value_to_svg( + prev=wire[index - 1] if index > 0 else wire[index], + value=wire[index], + width=wire.width(), + left=left + ((index - start) * (self.style.FULL_WIDTH / length)) + self.style.TEXT_WIDTH, + top=top, + length=length, + initial=(index == start), + vector_radix=vector_radix, + ) + return svg + elif wire.name in visible_wires: + # Show only specified wires and their waveforms + svg = self._shape_to_svg( + { + "name": "text", + "x": left, + "y": top + 15, + "class": "small", + "fill": self.style.TEXT_COLOR, + "font-family": "monospace", + "content": html.escape( + wire.name if len(wire.name) <= self.style.LABEL_WIDTH else wire.name[: max(self.style.LABEL_WIDTH - 3, 0)] + "...", + ), + } + ) + for index in range(start, start + length): + if (user_start is None or index >= user_start) and (user_end is None or index <= user_end): + # Show waveform only for specified wires + svg += self._value_to_svg( + prev=wire[index - 1] if index > 0 else wire[index], + value=wire[index], + width=wire.width(), + left=left + ((index - start) * (self.style.FULL_WIDTH / length)) + self.style.TEXT_WIDTH, + top=top, + length=length, + initial=(index == start), + vector_radix=vector_radix, + ) + return svg + else: + return "" # Return an empty string for invisible wires class ValueType(Enum): LOW = 0