Skip to content

Add changed byte highlighting to viewer.py #1159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Nov 16, 2021
Merged
110 changes: 64 additions & 46 deletions can/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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]
] = {}
Expand Down
Binary file added doc/images/viewer_changed_bytes_highlighting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion doc/scripts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^^^^^^^
Expand Down
7 changes: 6 additions & 1 deletion test/test_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down