Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 141 additions & 98 deletions can/io/asc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- under `test/data/logfile.asc`
"""

from typing import cast, Any, Generator, IO, List, Optional, Tuple, Union
from typing import cast, Any, Generator, IO, List, Optional, Union, Dict
from can import typechecking

from datetime import datetime
Expand Down Expand Up @@ -53,122 +53,165 @@ def __init__(
if not self.file:
raise ValueError("The given file cannot be None")
self.base = base
self._converted_base = self._check_base(base)
self.date = None
self.timestamps_format = None
self.internal_events_logged = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be used anywhere. It seems to just indicate the presence of the following line:

internal events logged

Can you help me understand why this needs to be exposed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karlding The line can be either "internal events logged" or "no internal events logged". It may be useful in the future as we add support for more kinds of frames/events in the AscReader.


@staticmethod
def _extract_can_id(str_can_id: str, base: int) -> Tuple[int, bool]:
def _extract_header(self):
for line in self.file:
line = line.strip()
lower_case = line.lower()
if lower_case.startswith("date"):
self.date = line[5:]
elif lower_case.startswith("base"):
try:
_, base, _, timestamp_format = line.split()
except ValueError:
raise Exception("Unsupported header string format: {}".format(line))
self.base = base
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __init__ takes a given base, whereas here right after that we're potentially overwriting it. What happens if the constructor specified hex, but the file is dec?

If you were intending to use this value from the .asc to validate, I don't think you should've overwritten self.base on this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karlding In both the current and previous implementations, we were ignoring the base passed on to the constructor and using the base defined in the ASCII logs file to parse all the messages. It seems accurate to set base to the value actually used during parsing so I think overwriting it is fine.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, if the idea is for the ASCReader to parse the value from the file, then we should probably just deprecate those parameters in __init__ and raise a DeprecationWarning or something.

I don't have access to Vector's tools so I can't check, but are there cases where an ASC file is considered valid but it doesn't specify a base?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the only use case I can think of is when we have a logs file split across two ASCII files. Not completely sure if the header is present in files other than the first file. If the intention is perhaps to read only the second file, then having a manually specified base can be useful. Your call.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just leave this for now then. If someone has a use case for this, we can revisit it at that point.

self._converted_base = self._check_base(self.base)
self.timestamps_format = timestamp_format
elif lower_case.endswith("internal events logged"):
self.internal_events_logged = not lower_case.startswith("no")
# Currently the last line in the header which is parsed
break
else:
break

def _extract_can_id(self, str_can_id: str, msg_kwargs: Dict[str, Any]) -> None:
if str_can_id[-1:].lower() == "x":
is_extended = True
can_id = int(str_can_id[0:-1], base)
msg_kwargs["is_extended_id"] = True
can_id = int(str_can_id[0:-1], self._converted_base)
else:
is_extended = False
can_id = int(str_can_id, base)
return can_id, is_extended
msg_kwargs["is_extended_id"] = False
can_id = int(str_can_id, self._converted_base)
msg_kwargs["arbitration_id"] = can_id

@staticmethod
def _check_base(base: str) -> int:
if base not in ["hex", "dec"]:
raise ValueError('base should be either "hex" or "dec"')
return BASE_DEC if base == "dec" else BASE_HEX

def _process_data_string(
self, data_str: str, data_length: int, msg_kwargs: Dict[str, Any]
) -> None:
frame = bytearray()
data = data_str.split()
for byte in data[:data_length]:
frame.append(int(byte, self._converted_base))
msg_kwargs["data"] = frame

def _process_classic_can_frame(
self, line: str, msg_kwargs: Dict[str, Any]
) -> Message:

# CAN error frame
if line.strip()[0:10].lower() == "errorframe":
# Error Frame
msg_kwargs["is_error_frame"] = True
else:
abr_id_str, dir, rest_of_message = line.split(None, 2)
msg_kwargs["is_rx"] = dir == "Rx"
self._extract_can_id(abr_id_str, msg_kwargs)

if rest_of_message[0].lower() == "r":
# CAN Remote Frame
msg_kwargs["is_remote_frame"] = True
remote_data = rest_of_message.split()
if len(remote_data) > 1:
dlc_str = remote_data[1]
if dlc_str.isdigit():
msg_kwargs["dlc"] = int(dlc_str, self._converted_base)
else:
# Classic CAN Message
try:
# There is data after DLC
_, dlc_str, data = rest_of_message.split(None, 2)
except ValueError:
# No data after DLC
_, dlc_str = rest_of_message.split(None, 1)
data = ""

dlc = int(dlc_str, self._converted_base)
msg_kwargs["dlc"] = dlc
self._process_data_string(data, dlc, msg_kwargs)

return Message(**msg_kwargs)

def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Message:
channel, dir, rest_of_message = line.split(None, 2)
# See ASCWriter
msg_kwargs["channel"] = int(channel) - 1
msg_kwargs["is_rx"] = dir == "Rx"

# CAN FD error frame
if rest_of_message.strip()[:10].lower() == "errorframe":
# Error Frame
# TODO: maybe use regex to parse BRS, ESI, etc?
msg_kwargs["is_error_frame"] = True
else:
can_id_str, frame_name_or_brs, rest_of_message = rest_of_message.split(
None, 2
)

if frame_name_or_brs.isdigit():
brs = frame_name_or_brs
esi, dlc_str, data_length_str, data = rest_of_message.split(None, 3)
else:
brs, esi, dlc_str, data_length_str, data = rest_of_message.split(
None, 4
)

self._extract_can_id(can_id_str, msg_kwargs)
msg_kwargs["bitrate_switch"] = brs == "1"
msg_kwargs["error_state_indicator"] = esi == "1"
dlc = int(dlc_str, self._converted_base)
msg_kwargs["dlc"] = dlc
data_length = int(data_length_str)

# CAN remote Frame
msg_kwargs["is_remote_frame"] = data_length == 0

self._process_data_string(data, data_length, msg_kwargs)

return Message(**msg_kwargs)

def __iter__(self) -> Generator[Message, None, None]:
base = self._check_base(self.base)
# This is guaranteed to not be None since we raise ValueError in __init__
self.file = cast(IO[Any], self.file)
for line in self.file:
# logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0])
if line.split(" ")[0] == "base":
base = self._check_base(line.split(" ")[1])
self._extract_header()

for line in self.file:
temp = line.strip()
if not temp or not temp[0].isdigit():
# Could be a comment
continue
is_fd = False
is_rx = True
msg_kwargs = {}
try:
timestamp, channel, dummy = temp.split(
None, 2
) # , frameType, dlc, frameData
timestamp, channel, rest_of_message = temp.split(None, 2)
timestamp = float(timestamp)
msg_kwargs["timestamp"] = timestamp
if channel == "CANFD":
timestamp, _, channel, direction, dummy = temp.split(None, 4)
is_fd = True
is_rx = direction == "Rx"
msg_kwargs["is_fd"] = True
elif channel.isdigit():
# See ASCWriter
msg_kwargs["channel"] = int(channel) - 1
else:
# Not a CAN message. Possible values include "statistic", J1939TP
continue
except ValueError:
# we parsed an empty comment
# Some other unprocessed or unknown format
continue
timestamp = float(timestamp)
try:
# See ASCWriter
channel = int(channel) - 1
except ValueError:
pass
if dummy.strip()[0:10].lower() == "errorframe":
msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel)
yield msg
elif (
not isinstance(channel, int)
or dummy.strip()[0:10].lower() == "statistic:"
or dummy.split(None, 1)[0] == "J1939TP"
):
pass
elif dummy[-1:].lower() == "r":
can_id_str, direction, _ = dummy.split(None, 2)
can_id_num, is_extended_id = self._extract_can_id(can_id_str, base)
msg = Message(
timestamp=timestamp,
arbitration_id=can_id_num & CAN_ID_MASK,
is_extended_id=is_extended_id,
is_remote_frame=True,
is_rx=direction == "Rx",
channel=channel,
)
yield msg

if "is_fd" not in msg_kwargs:
msg = self._process_classic_can_frame(rest_of_message, msg_kwargs)
else:
brs = None
esi = None
data_length = 0
try:
# this only works if dlc > 0 and thus data is available
if not is_fd:
can_id_str, direction, _, dlc, data = dummy.split(None, 4)
is_rx = direction == "Rx"
else:
can_id_str, frame_name, brs, esi, dlc, data_length, data = dummy.split(
None, 6
)
if frame_name.isdigit():
# Empty frame_name
can_id_str, brs, esi, dlc, data_length, data = dummy.split(
None, 5
)
except ValueError:
# but if not, we only want to get the stuff up to the dlc
can_id_str, _, _, dlc = dummy.split(None, 3)
# and we set data to an empty sequence manually
data = ""
dlc = int(dlc, base)
if is_fd:
# For fd frames, dlc and data length might not be equal and
# data_length is the actual size of the data
dlc = int(data_length)
frame = bytearray()
data = data.split()
for byte in data[0:dlc]:
frame.append(int(byte, base))
can_id_num, is_extended_id = self._extract_can_id(can_id_str, base)

yield Message(
timestamp=timestamp,
arbitration_id=can_id_num & CAN_ID_MASK,
is_extended_id=is_extended_id,
is_remote_frame=False,
dlc=dlc,
data=frame,
is_fd=is_fd,
is_rx=is_rx,
channel=channel,
bitrate_switch=is_fd and brs == "1",
error_state_indicator=is_fd and esi == "1",
)
msg = self._process_fd_can_frame(rest_of_message, msg_kwargs)
if msg is not None:
yield msg

self.stop()


Expand All @@ -190,7 +233,7 @@ class ASCWriter(BaseIOHandler, Listener):
"{id:>8} {symbolic_name:>32}",
"{brs}",
"{esi}",
"{dlc}",
"{dlc:x}",
"{data_length:>2}",
"{data}",
"{message_duration:>8}",
Expand Down Expand Up @@ -281,10 +324,10 @@ def on_message_received(self, msg: Message) -> None:
self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp)
return
if msg.is_remote_frame:
dtype = "r"
dtype = "r {:x}".format(msg.dlc) # New after v8.5
data: List[str] = []
else:
dtype = "d {}".format(msg.dlc)
dtype = "d {:x}".format(msg.dlc)
data = ["{:02X}".format(byte) for byte in msg.data]
arb_id = "{:X}".format(msg.arbitration_id)
if msg.is_extended_id:
Expand Down
6 changes: 4 additions & 2 deletions test/data/example_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ def sort_messages(messages):
[
Message(is_fd=True, data=range(64)),
Message(is_fd=True, data=range(8)),
Message(is_fd=True, bitrate_switch=True),
Message(is_fd=True, error_state_indicator=True),
Message(is_fd=True, bitrate_switch=True, is_remote_frame=True),
Message(is_fd=True, error_state_indicator=True, is_remote_frame=True),
Message(is_fd=True, data=range(8), bitrate_switch=True),
Message(is_fd=True, data=range(8), error_state_indicator=True),
]
)

Expand Down
10 changes: 10 additions & 0 deletions test/data/logfile.asc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Begin Triggerblock Sam Sep 30 15:06:13.191 2017
1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
2.501000 1 ErrorFrame
2.501010 1 ErrorFrame ECC: 10100010
2.501020 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300
2.510001 2 100 Tx r
2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x
2.584921 4 300 Tx r 8 Length = 1704000 BitCount = 145 ID = 88888888x
3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x
3.148421 1 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 271910 BitCount = 140 ID = 418119424x
3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
Expand All @@ -23,6 +29,10 @@ Begin Triggerblock Sam Sep 30 15:06:13.191 2017
20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x
20.305233 2 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF
30.005071 CANFD 2 Rx 300 Generic_Name_12 1 0 8 8 01 02 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
30.300981 CANFD 3 Tx 50005x 0 0 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205
30.506898 CANFD 4 Rx 4EE 0 0 f 64 01 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205
30.806898 CANFD 5 Tx ErrorFrame Not Acknowledge error, dominant error flag fffe c7 31ca Arb. 556 44 0 0 f 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1331984 11 0 46500250 460a0250 20011736 20010205
113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
End TriggerBlock
11 changes: 11 additions & 0 deletions test/data/test_CanErrorFrames.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
date Sam Sep 30 15:06:13.191 2017
base hex timestamps absolute
internal events logged
// version 9.0.0
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
0.000000 Start of measurement
2.501000 1 ErrorFrame
3.501000 1 ErrorFrame ECC: 10100010
4.501000 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300
30.806898 CANFD 5 Tx ErrorFrame Not Acknowledge error, dominant error flag fffe c7 31ca Arb. 556 44 0 0 f 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1331984 11 0 46500250 460a0250 20011736 20010205
End TriggerBlock
10 changes: 10 additions & 0 deletions test/data/test_CanFdMessage.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
date Sam Sep 30 15:06:13.191 2017
base hex timestamps absolute
internal events logged
// version 9.0.0
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
0.000000 Start of measurement
30.005021 CANFD 1 Rx 300 1 0 8 8 11 c2 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
30.005041 CANFD 2 Tx 1C4D80A7x 0 1 8 8 12 c2 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
30.005071 CANFD 3 Rx 30a Generic_Name_12 1 1 8 8 01 02 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
End TriggerBlock
9 changes: 9 additions & 0 deletions test/data/test_CanFdMessage64.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
date Sam Sep 30 15:06:13.191 2017
base hex timestamps absolute
internal events logged
// version 9.0.0
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
0.000000 Start of measurement
30.506898 CANFD 4 Rx 4EE 0 1 f 64 A1 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205
31.506898 CANFD 4 Rx 1C4D80A7x AlphaNumericName_2 1 0 f 64 b1 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205
End TriggerBlock
8 changes: 8 additions & 0 deletions test/data/test_CanFdRemoteMessage.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
date Sam Sep 30 15:06:13.191 2017
base hex timestamps absolute
internal events logged
// version 9.0.0
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
0.000000 Start of measurement
30.300981 CANFD 3 Tx 50005x 0 1 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205
End TriggerBlock
9 changes: 9 additions & 0 deletions test/data/test_CanMessage.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
date Sam Sep 30 15:06:13.191 2017
base hex timestamps absolute
internal events logged
// version 9.0.0
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
0.000000 Start of measurement
2.5010 2 C8 Tx d 8 09 08 07 06 05 04 03 02
17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785
End TriggerBlock
10 changes: 10 additions & 0 deletions test/data/test_CanRemoteMessage.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
date Sam Sep 30 15:06:13.191 2017
base hex timestamps absolute
internal events logged
// version 9.0.0
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
0.000000 Start of measurement
2.510001 2 100 Rx r
2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x
2.584921 4 300 Rx r 8 Length = 1704000 BitCount = 145 ID = 88888888x
End TriggerBlock
Loading