Skip to content

Commit 4ac9008

Browse files
Add changed byte highlighting to viewer.py (#1159)
* Added optional byte highlighting to viewer Added optional byte highlighting that can be toggled using the shortcut key 'h' * Some black formatting changes * Final black formatting * Fixed viewer bug Fix for the following error since the introduction of #1151 / #1142 Traceback (most recent call last): File "/usr/lib/python3.7/runpy.py", line 193, in _run_module_as_main "__main__", mod_spec) File "/usr/lib/python3.7/runpy.py", line 85, in _run_code exec(code, run_globals) File "/home/pi/python-can/can/viewer.py", line 582, in <module> main() File "/home/pi/python-can/can/viewer.py", line 573, in main bus = _create_bus(parsed_args, **additional_config, app_name="python-can viewer") File "/home/pi/python-can/can/logger.py", line 105, in _create_bus if parsed_args.app_name: AttributeError: 'Namespace' object has no attribute 'app_name' * Update scripts.rst update viewer documentations * Update scripts.rst * Update scripts.rst arrange keyboard shortcuts into bulleted list * Update scripts.rst * Update can/viewer.py Co-authored-by: Felix Divo <[email protected]> * Better description. Add extra length to other lines in the help text so the border lines up. * Update test_viewer.py Add test coverage for some of the new byte highlighting functions. * Use _create_base_argument_parser in viewer.py Change standard arguments definition to be defined using logger._create_base_argument_parser instead of manually defining them in this module. Co-authored-by: Felix Divo <[email protected]>
1 parent 270224c commit 4ac9008

File tree

4 files changed

+85
-48
lines changed

4 files changed

+85
-48
lines changed

can/viewer.py

Lines changed: 64 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131

3232
import can
3333
from can import __version__
34-
from .logger import _create_bus, _parse_filters, _append_filter_argument
34+
from .logger import (
35+
_create_bus,
36+
_parse_filters,
37+
_append_filter_argument,
38+
_create_base_argument_parser,
39+
)
3540

3641

3742
logger = logging.getLogger("can.serial")
@@ -53,11 +58,14 @@ def __init__(self, stdscr, bus, data_structs, testing=False):
5358
self.bus = bus
5459
self.data_structs = data_structs
5560

56-
# Initialise the ID dictionary, start timestamp, scroll and variable for pausing the viewer
61+
# Initialise the ID dictionary, Previous values dict, start timestamp,
62+
# scroll and variables for pausing the viewer and enabling byte highlighting
5763
self.ids = {}
5864
self.start_time = None
5965
self.scroll = 0
6066
self.paused = False
67+
self.highlight_changed_bytes = False
68+
self.previous_values = {}
6169

6270
# Get the window dimensions - used for resizing the window
6371
self.y, self.x = self.stdscr.getmaxyx()
@@ -70,6 +78,8 @@ def __init__(self, stdscr, bus, data_structs, testing=False):
7078

7179
# Used to color error frames red
7280
curses.init_pair(1, curses.COLOR_RED, -1)
81+
# Used to color changed bytes
82+
curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLUE)
7383

7484
if not testing: # pragma: no cover
7585
self.run()
@@ -103,6 +113,14 @@ def run(self):
103113
self.scroll = 0
104114
self.draw_header()
105115

116+
# Toggle byte change highlighting pressing 'h'
117+
elif key == ord("h"):
118+
self.highlight_changed_bytes = not self.highlight_changed_bytes
119+
if not self.highlight_changed_bytes:
120+
# empty the previous values dict when leaving higlighting mode
121+
self.previous_values.clear()
122+
self.draw_header()
123+
106124
# Sort by pressing 's'
107125
elif key == ord("s"):
108126
# Sort frames based on the CAN-Bus ID
@@ -239,14 +257,36 @@ def draw_can_bus_message(self, msg, sorting=False):
239257
self.draw_line(self.ids[key]["row"], 23, f"{self.ids[key]['dt']:.6f}", color)
240258
self.draw_line(self.ids[key]["row"], 35, arbitration_id_string, color)
241259
self.draw_line(self.ids[key]["row"], 47, str(msg.dlc), color)
260+
261+
try:
262+
previous_byte_values = self.previous_values[key]
263+
except KeyError: # no row of previous values exists for the current message ID
264+
# initialise a row to store the values for comparison next time
265+
self.previous_values[key] = dict()
266+
previous_byte_values = self.previous_values[key]
242267
for i, b in enumerate(msg.data):
243268
col = 52 + i * 3
244269
if col > self.x - 2:
245270
# Data does not fit
246271
self.draw_line(self.ids[key]["row"], col - 4, "...", color)
247272
break
273+
if self.highlight_changed_bytes:
274+
try:
275+
if b != previous_byte_values[i]:
276+
# set colour to highlight a changed value
277+
data_color = curses.color_pair(2)
278+
else:
279+
data_color = color
280+
except KeyError:
281+
# previous entry for byte didnt exist - default to rest of line colour
282+
data_color = color
283+
finally:
284+
# write the new value to the previous values dict for next time
285+
previous_byte_values[i] = b
286+
else:
287+
data_color = color
248288
text = f"{b:02X}"
249-
self.draw_line(self.ids[key]["row"], col, text, color)
289+
self.draw_line(self.ids[key]["row"], col, text, data_color)
250290

251291
if self.data_structs:
252292
try:
@@ -284,7 +324,12 @@ def draw_header(self):
284324
self.draw_line(0, 35, "ID", curses.A_BOLD)
285325
self.draw_line(0, 47, "DLC", curses.A_BOLD)
286326
self.draw_line(0, 52, "Data", curses.A_BOLD)
287-
if self.data_structs: # Only draw if the dictionary is not empty
327+
328+
# Indicate that byte change highlighting is enabled
329+
if self.highlight_changed_bytes:
330+
self.draw_line(0, 57, "(changed)", curses.color_pair(2))
331+
# Only draw if the dictionary is not empty
332+
if self.data_structs:
288333
self.draw_line(0, 77, "Parsed values", curses.A_BOLD)
289334

290335
def redraw_screen(self):
@@ -345,20 +390,25 @@ def parse_args(args):
345390
"python -m can.viewer",
346391
description="A simple CAN viewer terminal application written in Python",
347392
epilog="R|Shortcuts: "
348-
"\n +---------+-------------------------+"
349-
"\n | Key | Description |"
350-
"\n +---------+-------------------------+"
351-
"\n | ESQ/q | Exit the viewer |"
352-
"\n | c | Clear the stored frames |"
353-
"\n | s | Sort the stored frames |"
354-
"\n | SPACE | Pause the viewer |"
355-
"\n | UP/DOWN | Scroll the viewer |"
356-
"\n +---------+-------------------------+",
393+
"\n +---------+-------------------------------+"
394+
"\n | Key | Description |"
395+
"\n +---------+-------------------------------+"
396+
"\n | ESQ/q | Exit the viewer |"
397+
"\n | c | Clear the stored frames |"
398+
"\n | s | Sort the stored frames |"
399+
"\n | h | Toggle highlight byte changes |"
400+
"\n | SPACE | Pause the viewer |"
401+
"\n | UP/DOWN | Scroll the viewer |"
402+
"\n +---------+-------------------------------+",
357403
formatter_class=SmartFormatter,
358404
add_help=False,
359405
allow_abbrev=False,
360406
)
361407

408+
# Generate the standard arguments:
409+
# Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support
410+
_create_base_argument_parser(parser)
411+
362412
optional = parser.add_argument_group("Optional arguments")
363413

364414
optional.add_argument(
@@ -372,31 +422,6 @@ def parse_args(args):
372422
version=f"%(prog)s (version {__version__})",
373423
)
374424

375-
# Copied from: can/logger.py
376-
optional.add_argument(
377-
"-b",
378-
"--bitrate",
379-
type=int,
380-
help="""Bitrate to use for the given CAN interface""",
381-
)
382-
383-
optional.add_argument("--fd", help="Activate CAN-FD support", action="store_true")
384-
385-
optional.add_argument(
386-
"--data_bitrate",
387-
type=int,
388-
help="Bitrate to use for the data phase in case of CAN-FD.",
389-
)
390-
391-
optional.add_argument(
392-
"-c",
393-
"--channel",
394-
help="""Most backend interfaces require some sort of channel.
395-
For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0"
396-
with the socketcan interfaces valid channel examples include: "can0", "vcan0".
397-
(default: use default for the specified interface)""",
398-
)
399-
400425
optional.add_argument(
401426
"-d",
402427
"--decode",
@@ -441,14 +466,6 @@ def parse_args(args):
441466

442467
_append_filter_argument(optional, "-f")
443468

444-
optional.add_argument(
445-
"-i",
446-
"--interface",
447-
dest="interface",
448-
help="R|Specify the backend CAN interface to use.",
449-
choices=sorted(can.VALID_INTERFACES),
450-
)
451-
452469
optional.add_argument(
453470
"-v",
454471
action="count",
@@ -486,6 +503,7 @@ def parse_args(args):
486503
# In order to convert from raw integer value the real units are multiplied with the values and
487504
# similarly the values
488505
# are divided by the value in order to convert from real units to raw integer values.
506+
489507
data_structs: Dict[
490508
Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]
491509
] = {}
19.9 KB
Loading

doc/scripts.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,21 @@ A screenshot of the application can be seen below:
2828
.. image:: images/viewer.png
2929
:width: 100%
3030

31-
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.
31+
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.
32+
There are several keyboard shortcuts that can be used with the viewer script, they function as follows:
33+
34+
* ESCAPE - Quit the viewer script
35+
* q - as ESCAPE
36+
* c - Clear the stored frames
37+
* s - Sort the stored frames
38+
* h - Toggle highlighting of changed bytes in the data field - see the below image
39+
* SPACE - Pause the viewer
40+
* UP/DOWN - Scroll the viewer
41+
42+
.. image:: images/viewer_changed_bytes_highlighting.png
43+
:width: 50%
44+
45+
A byte in the data field is highlighted blue if the value is different from the last time the message was received.
3246

3347
Command line arguments
3448
^^^^^^^^^^^^^^^^^^^^^^

test/test_viewer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ def getch(self):
102102
return curses.ascii.SP # Unpause
103103
elif self.key_counter == 5:
104104
return ord("s") # Sort
105-
105+
# Turn on byte highlighting (toggle)
106+
elif self.key_counter == 6:
107+
return ord("h")
108+
# Turn off byte highlighting (toggle)
109+
elif self.key_counter == 7:
110+
return ord("h")
106111
# Keep scrolling until it exceeds the number of messages
107112
elif self.key_counter <= 100:
108113
return curses.KEY_DOWN

0 commit comments

Comments
 (0)