diff --git a/can/viewer.py b/can/viewer.py index a92d18cdd..8fd3fc294 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -31,7 +31,12 @@ import can from can import __version__ -from .logger import _create_bus, _parse_filters, _append_filter_argument +from .logger import ( + _create_bus, + _parse_filters, + _append_filter_argument, + _create_base_argument_parser, +) logger = logging.getLogger("can.serial") @@ -53,11 +58,14 @@ def __init__(self, stdscr, bus, data_structs, testing=False): self.bus = bus self.data_structs = data_structs - # Initialise the ID dictionary, start timestamp, scroll and variable for pausing the viewer + # Initialise the ID dictionary, Previous values dict, start timestamp, + # scroll and variables for pausing the viewer and enabling byte highlighting self.ids = {} self.start_time = None self.scroll = 0 self.paused = False + self.highlight_changed_bytes = False + self.previous_values = {} # Get the window dimensions - used for resizing the window self.y, self.x = self.stdscr.getmaxyx() @@ -70,6 +78,8 @@ def __init__(self, stdscr, bus, data_structs, testing=False): # Used to color error frames red curses.init_pair(1, curses.COLOR_RED, -1) + # Used to color changed bytes + curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLUE) if not testing: # pragma: no cover self.run() @@ -103,6 +113,14 @@ def run(self): self.scroll = 0 self.draw_header() + # Toggle byte change highlighting pressing 'h' + elif key == ord("h"): + self.highlight_changed_bytes = not self.highlight_changed_bytes + if not self.highlight_changed_bytes: + # empty the previous values dict when leaving higlighting mode + self.previous_values.clear() + self.draw_header() + # Sort by pressing 's' elif key == ord("s"): # Sort frames based on the CAN-Bus ID @@ -239,14 +257,36 @@ def draw_can_bus_message(self, msg, sorting=False): self.draw_line(self.ids[key]["row"], 23, f"{self.ids[key]['dt']:.6f}", color) self.draw_line(self.ids[key]["row"], 35, arbitration_id_string, color) self.draw_line(self.ids[key]["row"], 47, str(msg.dlc), color) + + try: + previous_byte_values = self.previous_values[key] + except KeyError: # no row of previous values exists for the current message ID + # initialise a row to store the values for comparison next time + self.previous_values[key] = dict() + previous_byte_values = self.previous_values[key] for i, b in enumerate(msg.data): col = 52 + i * 3 if col > self.x - 2: # Data does not fit self.draw_line(self.ids[key]["row"], col - 4, "...", color) break + if self.highlight_changed_bytes: + try: + if b != previous_byte_values[i]: + # set colour to highlight a changed value + data_color = curses.color_pair(2) + else: + data_color = color + except KeyError: + # previous entry for byte didnt exist - default to rest of line colour + data_color = color + finally: + # write the new value to the previous values dict for next time + previous_byte_values[i] = b + else: + data_color = color text = f"{b:02X}" - self.draw_line(self.ids[key]["row"], col, text, color) + self.draw_line(self.ids[key]["row"], col, text, data_color) if self.data_structs: try: @@ -284,7 +324,12 @@ def draw_header(self): self.draw_line(0, 35, "ID", curses.A_BOLD) self.draw_line(0, 47, "DLC", curses.A_BOLD) self.draw_line(0, 52, "Data", curses.A_BOLD) - if self.data_structs: # Only draw if the dictionary is not empty + + # Indicate that byte change highlighting is enabled + if self.highlight_changed_bytes: + self.draw_line(0, 57, "(changed)", curses.color_pair(2)) + # Only draw if the dictionary is not empty + if self.data_structs: self.draw_line(0, 77, "Parsed values", curses.A_BOLD) def redraw_screen(self): @@ -345,20 +390,25 @@ def parse_args(args): "python -m can.viewer", description="A simple CAN viewer terminal application written in Python", epilog="R|Shortcuts: " - "\n +---------+-------------------------+" - "\n | Key | Description |" - "\n +---------+-------------------------+" - "\n | ESQ/q | Exit the viewer |" - "\n | c | Clear the stored frames |" - "\n | s | Sort the stored frames |" - "\n | SPACE | Pause the viewer |" - "\n | UP/DOWN | Scroll the viewer |" - "\n +---------+-------------------------+", + "\n +---------+-------------------------------+" + "\n | Key | Description |" + "\n +---------+-------------------------------+" + "\n | ESQ/q | Exit the viewer |" + "\n | c | Clear the stored frames |" + "\n | s | Sort the stored frames |" + "\n | h | Toggle highlight byte changes |" + "\n | SPACE | Pause the viewer |" + "\n | UP/DOWN | Scroll the viewer |" + "\n +---------+-------------------------------+", formatter_class=SmartFormatter, add_help=False, allow_abbrev=False, ) + # Generate the standard arguments: + # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support + _create_base_argument_parser(parser) + optional = parser.add_argument_group("Optional arguments") optional.add_argument( @@ -372,31 +422,6 @@ def parse_args(args): version=f"%(prog)s (version {__version__})", ) - # Copied from: can/logger.py - optional.add_argument( - "-b", - "--bitrate", - type=int, - help="""Bitrate to use for the given CAN interface""", - ) - - optional.add_argument("--fd", help="Activate CAN-FD support", action="store_true") - - optional.add_argument( - "--data_bitrate", - type=int, - help="Bitrate to use for the data phase in case of CAN-FD.", - ) - - optional.add_argument( - "-c", - "--channel", - help="""Most backend interfaces require some sort of channel. - For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" - with the socketcan interfaces valid channel examples include: "can0", "vcan0". - (default: use default for the specified interface)""", - ) - optional.add_argument( "-d", "--decode", @@ -441,14 +466,6 @@ def parse_args(args): _append_filter_argument(optional, "-f") - optional.add_argument( - "-i", - "--interface", - dest="interface", - help="R|Specify the backend CAN interface to use.", - choices=sorted(can.VALID_INTERFACES), - ) - optional.add_argument( "-v", action="count", @@ -486,6 +503,7 @@ def parse_args(args): # In order to convert from raw integer value the real units are multiplied with the values and # similarly the values # are divided by the value in order to convert from real units to raw integer values. + data_structs: Dict[ Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None] ] = {} diff --git a/doc/images/viewer_changed_bytes_highlighting.png b/doc/images/viewer_changed_bytes_highlighting.png new file mode 100644 index 000000000..53e838488 Binary files /dev/null and b/doc/images/viewer_changed_bytes_highlighting.png differ diff --git a/doc/scripts.rst b/doc/scripts.rst index a63f1b108..2b8607667 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -28,7 +28,21 @@ A screenshot of the application can be seen below: .. image:: images/viewer.png :width: 100% -The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. +The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. +There are several keyboard shortcuts that can be used with the viewer script, they function as follows: + +* ESCAPE - Quit the viewer script +* q - as ESCAPE +* c - Clear the stored frames +* s - Sort the stored frames +* h - Toggle highlighting of changed bytes in the data field - see the below image +* SPACE - Pause the viewer +* UP/DOWN - Scroll the viewer + +.. image:: images/viewer_changed_bytes_highlighting.png + :width: 50% + +A byte in the data field is highlighted blue if the value is different from the last time the message was received. Command line arguments ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/test/test_viewer.py b/test/test_viewer.py index 32ea69691..9b74add6b 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -102,7 +102,12 @@ def getch(self): return curses.ascii.SP # Unpause elif self.key_counter == 5: return ord("s") # Sort - + # Turn on byte highlighting (toggle) + elif self.key_counter == 6: + return ord("h") + # Turn off byte highlighting (toggle) + elif self.key_counter == 7: + return ord("h") # Keep scrolling until it exceeds the number of messages elif self.key_counter <= 100: return curses.KEY_DOWN