Skip to content

Commit 30ef0c3

Browse files
authored
ASCII Reader/Writer enhancements (#820)
Added support for new formats for CAN remote frames Reworked and cleaned existing implementation Some minor fixes (e.g. dlc != len(data) for FD frames) Added parsing ASCII header parsing
1 parent ef2ec5f commit 30ef0c3

File tree

10 files changed

+363
-100
lines changed

10 files changed

+363
-100
lines changed

can/io/asc.py

Lines changed: 141 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- under `test/data/logfile.asc`
77
"""
88

9-
from typing import cast, Any, Generator, IO, List, Optional, Tuple, Union
9+
from typing import cast, Any, Generator, IO, List, Optional, Union, Dict
1010
from can import typechecking
1111

1212
from datetime import datetime
@@ -53,122 +53,165 @@ def __init__(
5353
if not self.file:
5454
raise ValueError("The given file cannot be None")
5555
self.base = base
56+
self._converted_base = self._check_base(base)
57+
self.date = None
58+
self.timestamps_format = None
59+
self.internal_events_logged = None
5660

57-
@staticmethod
58-
def _extract_can_id(str_can_id: str, base: int) -> Tuple[int, bool]:
61+
def _extract_header(self):
62+
for line in self.file:
63+
line = line.strip()
64+
lower_case = line.lower()
65+
if lower_case.startswith("date"):
66+
self.date = line[5:]
67+
elif lower_case.startswith("base"):
68+
try:
69+
_, base, _, timestamp_format = line.split()
70+
except ValueError:
71+
raise Exception("Unsupported header string format: {}".format(line))
72+
self.base = base
73+
self._converted_base = self._check_base(self.base)
74+
self.timestamps_format = timestamp_format
75+
elif lower_case.endswith("internal events logged"):
76+
self.internal_events_logged = not lower_case.startswith("no")
77+
# Currently the last line in the header which is parsed
78+
break
79+
else:
80+
break
81+
82+
def _extract_can_id(self, str_can_id: str, msg_kwargs: Dict[str, Any]) -> None:
5983
if str_can_id[-1:].lower() == "x":
60-
is_extended = True
61-
can_id = int(str_can_id[0:-1], base)
84+
msg_kwargs["is_extended_id"] = True
85+
can_id = int(str_can_id[0:-1], self._converted_base)
6286
else:
63-
is_extended = False
64-
can_id = int(str_can_id, base)
65-
return can_id, is_extended
87+
msg_kwargs["is_extended_id"] = False
88+
can_id = int(str_can_id, self._converted_base)
89+
msg_kwargs["arbitration_id"] = can_id
6690

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

97+
def _process_data_string(
98+
self, data_str: str, data_length: int, msg_kwargs: Dict[str, Any]
99+
) -> None:
100+
frame = bytearray()
101+
data = data_str.split()
102+
for byte in data[:data_length]:
103+
frame.append(int(byte, self._converted_base))
104+
msg_kwargs["data"] = frame
105+
106+
def _process_classic_can_frame(
107+
self, line: str, msg_kwargs: Dict[str, Any]
108+
) -> Message:
109+
110+
# CAN error frame
111+
if line.strip()[0:10].lower() == "errorframe":
112+
# Error Frame
113+
msg_kwargs["is_error_frame"] = True
114+
else:
115+
abr_id_str, dir, rest_of_message = line.split(None, 2)
116+
msg_kwargs["is_rx"] = dir == "Rx"
117+
self._extract_can_id(abr_id_str, msg_kwargs)
118+
119+
if rest_of_message[0].lower() == "r":
120+
# CAN Remote Frame
121+
msg_kwargs["is_remote_frame"] = True
122+
remote_data = rest_of_message.split()
123+
if len(remote_data) > 1:
124+
dlc_str = remote_data[1]
125+
if dlc_str.isdigit():
126+
msg_kwargs["dlc"] = int(dlc_str, self._converted_base)
127+
else:
128+
# Classic CAN Message
129+
try:
130+
# There is data after DLC
131+
_, dlc_str, data = rest_of_message.split(None, 2)
132+
except ValueError:
133+
# No data after DLC
134+
_, dlc_str = rest_of_message.split(None, 1)
135+
data = ""
136+
137+
dlc = int(dlc_str, self._converted_base)
138+
msg_kwargs["dlc"] = dlc
139+
self._process_data_string(data, dlc, msg_kwargs)
140+
141+
return Message(**msg_kwargs)
142+
143+
def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Message:
144+
channel, dir, rest_of_message = line.split(None, 2)
145+
# See ASCWriter
146+
msg_kwargs["channel"] = int(channel) - 1
147+
msg_kwargs["is_rx"] = dir == "Rx"
148+
149+
# CAN FD error frame
150+
if rest_of_message.strip()[:10].lower() == "errorframe":
151+
# Error Frame
152+
# TODO: maybe use regex to parse BRS, ESI, etc?
153+
msg_kwargs["is_error_frame"] = True
154+
else:
155+
can_id_str, frame_name_or_brs, rest_of_message = rest_of_message.split(
156+
None, 2
157+
)
158+
159+
if frame_name_or_brs.isdigit():
160+
brs = frame_name_or_brs
161+
esi, dlc_str, data_length_str, data = rest_of_message.split(None, 3)
162+
else:
163+
brs, esi, dlc_str, data_length_str, data = rest_of_message.split(
164+
None, 4
165+
)
166+
167+
self._extract_can_id(can_id_str, msg_kwargs)
168+
msg_kwargs["bitrate_switch"] = brs == "1"
169+
msg_kwargs["error_state_indicator"] = esi == "1"
170+
dlc = int(dlc_str, self._converted_base)
171+
msg_kwargs["dlc"] = dlc
172+
data_length = int(data_length_str)
173+
174+
# CAN remote Frame
175+
msg_kwargs["is_remote_frame"] = data_length == 0
176+
177+
self._process_data_string(data, data_length, msg_kwargs)
178+
179+
return Message(**msg_kwargs)
180+
73181
def __iter__(self) -> Generator[Message, None, None]:
74-
base = self._check_base(self.base)
75182
# This is guaranteed to not be None since we raise ValueError in __init__
76183
self.file = cast(IO[Any], self.file)
77-
for line in self.file:
78-
# logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0])
79-
if line.split(" ")[0] == "base":
80-
base = self._check_base(line.split(" ")[1])
184+
self._extract_header()
81185

186+
for line in self.file:
82187
temp = line.strip()
83188
if not temp or not temp[0].isdigit():
189+
# Could be a comment
84190
continue
85-
is_fd = False
86-
is_rx = True
191+
msg_kwargs = {}
87192
try:
88-
timestamp, channel, dummy = temp.split(
89-
None, 2
90-
) # , frameType, dlc, frameData
193+
timestamp, channel, rest_of_message = temp.split(None, 2)
194+
timestamp = float(timestamp)
195+
msg_kwargs["timestamp"] = timestamp
91196
if channel == "CANFD":
92-
timestamp, _, channel, direction, dummy = temp.split(None, 4)
93-
is_fd = True
94-
is_rx = direction == "Rx"
197+
msg_kwargs["is_fd"] = True
198+
elif channel.isdigit():
199+
# See ASCWriter
200+
msg_kwargs["channel"] = int(channel) - 1
201+
else:
202+
# Not a CAN message. Possible values include "statistic", J1939TP
203+
continue
95204
except ValueError:
96-
# we parsed an empty comment
205+
# Some other unprocessed or unknown format
97206
continue
98-
timestamp = float(timestamp)
99-
try:
100-
# See ASCWriter
101-
channel = int(channel) - 1
102-
except ValueError:
103-
pass
104-
if dummy.strip()[0:10].lower() == "errorframe":
105-
msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel)
106-
yield msg
107-
elif (
108-
not isinstance(channel, int)
109-
or dummy.strip()[0:10].lower() == "statistic:"
110-
or dummy.split(None, 1)[0] == "J1939TP"
111-
):
112-
pass
113-
elif dummy[-1:].lower() == "r":
114-
can_id_str, direction, _ = dummy.split(None, 2)
115-
can_id_num, is_extended_id = self._extract_can_id(can_id_str, base)
116-
msg = Message(
117-
timestamp=timestamp,
118-
arbitration_id=can_id_num & CAN_ID_MASK,
119-
is_extended_id=is_extended_id,
120-
is_remote_frame=True,
121-
is_rx=direction == "Rx",
122-
channel=channel,
123-
)
124-
yield msg
207+
208+
if "is_fd" not in msg_kwargs:
209+
msg = self._process_classic_can_frame(rest_of_message, msg_kwargs)
125210
else:
126-
brs = None
127-
esi = None
128-
data_length = 0
129-
try:
130-
# this only works if dlc > 0 and thus data is available
131-
if not is_fd:
132-
can_id_str, direction, _, dlc, data = dummy.split(None, 4)
133-
is_rx = direction == "Rx"
134-
else:
135-
can_id_str, frame_name, brs, esi, dlc, data_length, data = dummy.split(
136-
None, 6
137-
)
138-
if frame_name.isdigit():
139-
# Empty frame_name
140-
can_id_str, brs, esi, dlc, data_length, data = dummy.split(
141-
None, 5
142-
)
143-
except ValueError:
144-
# but if not, we only want to get the stuff up to the dlc
145-
can_id_str, _, _, dlc = dummy.split(None, 3)
146-
# and we set data to an empty sequence manually
147-
data = ""
148-
dlc = int(dlc, base)
149-
if is_fd:
150-
# For fd frames, dlc and data length might not be equal and
151-
# data_length is the actual size of the data
152-
dlc = int(data_length)
153-
frame = bytearray()
154-
data = data.split()
155-
for byte in data[0:dlc]:
156-
frame.append(int(byte, base))
157-
can_id_num, is_extended_id = self._extract_can_id(can_id_str, base)
158-
159-
yield Message(
160-
timestamp=timestamp,
161-
arbitration_id=can_id_num & CAN_ID_MASK,
162-
is_extended_id=is_extended_id,
163-
is_remote_frame=False,
164-
dlc=dlc,
165-
data=frame,
166-
is_fd=is_fd,
167-
is_rx=is_rx,
168-
channel=channel,
169-
bitrate_switch=is_fd and brs == "1",
170-
error_state_indicator=is_fd and esi == "1",
171-
)
211+
msg = self._process_fd_can_frame(rest_of_message, msg_kwargs)
212+
if msg is not None:
213+
yield msg
214+
172215
self.stop()
173216

174217

@@ -190,7 +233,7 @@ class ASCWriter(BaseIOHandler, Listener):
190233
"{id:>8} {symbolic_name:>32}",
191234
"{brs}",
192235
"{esi}",
193-
"{dlc}",
236+
"{dlc:x}",
194237
"{data_length:>2}",
195238
"{data}",
196239
"{message_duration:>8}",
@@ -281,10 +324,10 @@ def on_message_received(self, msg: Message) -> None:
281324
self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp)
282325
return
283326
if msg.is_remote_frame:
284-
dtype = "r"
327+
dtype = "r {:x}".format(msg.dlc) # New after v8.5
285328
data: List[str] = []
286329
else:
287-
dtype = "d {}".format(msg.dlc)
330+
dtype = "d {:x}".format(msg.dlc)
288331
data = ["{:02X}".format(byte) for byte in msg.data]
289332
arb_id = "{:X}".format(msg.arbitration_id)
290333
if msg.is_extended_id:

test/data/example_data.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ def sort_messages(messages):
112112
[
113113
Message(is_fd=True, data=range(64)),
114114
Message(is_fd=True, data=range(8)),
115-
Message(is_fd=True, bitrate_switch=True),
116-
Message(is_fd=True, error_state_indicator=True),
115+
Message(is_fd=True, bitrate_switch=True, is_remote_frame=True),
116+
Message(is_fd=True, error_state_indicator=True, is_remote_frame=True),
117+
Message(is_fd=True, data=range(8), bitrate_switch=True),
118+
Message(is_fd=True, data=range(8), error_state_indicator=True),
117119
]
118120
)
119121

test/data/logfile.asc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Begin Triggerblock Sam Sep 30 15:06:13.191 2017
99
1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
1010
1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
1111
2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
12+
2.501000 1 ErrorFrame
13+
2.501010 1 ErrorFrame ECC: 10100010
14+
2.501020 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300
15+
2.510001 2 100 Tx r
16+
2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x
17+
2.584921 4 300 Tx r 8 Length = 1704000 BitCount = 145 ID = 88888888x
1218
3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x
1319
3.148421 1 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 271910 BitCount = 140 ID = 418119424x
1420
3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
@@ -23,6 +29,10 @@ Begin Triggerblock Sam Sep 30 15:06:13.191 2017
2329
20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
2430
20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x
2531
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
32+
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
33+
30.300981 CANFD 3 Tx 50005x 0 0 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205
34+
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
35+
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
2636
113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
2737
113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
2838
End TriggerBlock

test/data/test_CanErrorFrames.asc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
date Sam Sep 30 15:06:13.191 2017
2+
base hex timestamps absolute
3+
internal events logged
4+
// version 9.0.0
5+
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
6+
0.000000 Start of measurement
7+
2.501000 1 ErrorFrame
8+
3.501000 1 ErrorFrame ECC: 10100010
9+
4.501000 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300
10+
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
11+
End TriggerBlock

test/data/test_CanFdMessage.asc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
date Sam Sep 30 15:06:13.191 2017
2+
base hex timestamps absolute
3+
internal events logged
4+
// version 9.0.0
5+
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
6+
0.000000 Start of measurement
7+
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
8+
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
9+
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
10+
End TriggerBlock

test/data/test_CanFdMessage64.asc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
date Sam Sep 30 15:06:13.191 2017
2+
base hex timestamps absolute
3+
internal events logged
4+
// version 9.0.0
5+
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
6+
0.000000 Start of measurement
7+
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
8+
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
9+
End TriggerBlock
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
date Sam Sep 30 15:06:13.191 2017
2+
base hex timestamps absolute
3+
internal events logged
4+
// version 9.0.0
5+
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
6+
0.000000 Start of measurement
7+
30.300981 CANFD 3 Tx 50005x 0 1 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205
8+
End TriggerBlock

test/data/test_CanMessage.asc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
date Sam Sep 30 15:06:13.191 2017
2+
base hex timestamps absolute
3+
internal events logged
4+
// version 9.0.0
5+
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
6+
0.000000 Start of measurement
7+
2.5010 2 C8 Tx d 8 09 08 07 06 05 04 03 02
8+
17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785
9+
End TriggerBlock
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
date Sam Sep 30 15:06:13.191 2017
2+
base hex timestamps absolute
3+
internal events logged
4+
// version 9.0.0
5+
Begin Triggerblock Sam Sep 30 15:06:13.191 2017
6+
0.000000 Start of measurement
7+
2.510001 2 100 Rx r
8+
2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x
9+
2.584921 4 300 Rx r 8 Length = 1704000 BitCount = 145 ID = 88888888x
10+
End TriggerBlock

0 commit comments

Comments
 (0)