diff --git a/can/io/asc.py b/can/io/asc.py index 1ad3acef6..f28806478 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -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 @@ -53,16 +53,40 @@ 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 - @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 + 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: @@ -70,105 +94,124 @@ def _check_base(base: str) -> int: 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() @@ -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}", @@ -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: diff --git a/test/data/example_data.py b/test/data/example_data.py index 773212c32..c682d35f5 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -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), ] ) diff --git a/test/data/logfile.asc b/test/data/logfile.asc index 8582cbf05..77ebdb78a 100644 --- a/test/data/logfile.asc +++ b/test/data/logfile.asc @@ -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 @@ -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 diff --git a/test/data/test_CanErrorFrames.asc b/test/data/test_CanErrorFrames.asc new file mode 100644 index 000000000..0d400c18c --- /dev/null +++ b/test/data/test_CanErrorFrames.asc @@ -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 diff --git a/test/data/test_CanFdMessage.asc b/test/data/test_CanFdMessage.asc new file mode 100644 index 000000000..51fbcb1ce --- /dev/null +++ b/test/data/test_CanFdMessage.asc @@ -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 diff --git a/test/data/test_CanFdMessage64.asc b/test/data/test_CanFdMessage64.asc new file mode 100644 index 000000000..ab34ee7ae --- /dev/null +++ b/test/data/test_CanFdMessage64.asc @@ -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 diff --git a/test/data/test_CanFdRemoteMessage.asc b/test/data/test_CanFdRemoteMessage.asc new file mode 100644 index 000000000..7c78f5d06 --- /dev/null +++ b/test/data/test_CanFdRemoteMessage.asc @@ -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 diff --git a/test/data/test_CanMessage.asc b/test/data/test_CanMessage.asc new file mode 100644 index 000000000..52dda34d9 --- /dev/null +++ b/test/data/test_CanMessage.asc @@ -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 diff --git a/test/data/test_CanRemoteMessage.asc b/test/data/test_CanRemoteMessage.asc new file mode 100644 index 000000000..4e6431576 --- /dev/null +++ b/test/data/test_CanRemoteMessage.asc @@ -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 diff --git a/test/logformats_test.py b/test/logformats_test.py index 4a5c408b5..08a9e929d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -344,6 +344,157 @@ def _setup_instance(self): adds_default_channel=0, ) + def _read_log_file(self, filename): + logfile = os.path.join(os.path.dirname(__file__), "data", filename) + with can.ASCReader(logfile) as reader: + return list(reader) + + def test_can_message(self): + expected_messages = [ + can.Message( + timestamp=2.5010, + arbitration_id=0xC8, + is_extended_id=False, + is_rx=False, + channel=1, + dlc=8, + data=[9, 8, 7, 6, 5, 4, 3, 2], + ), + can.Message( + timestamp=17.876708, + arbitration_id=0x6F9, + is_extended_id=False, + channel=0, + dlc=0x8, + data=[5, 0xC, 0, 0, 0, 0, 0, 0], + ), + ] + actual = self._read_log_file("test_CanMessage.asc") + self.assertMessagesEqual(actual, expected_messages) + + def test_can_remote_message(self): + expected_messages = [ + can.Message( + timestamp=2.510001, + arbitration_id=0x100, + is_extended_id=False, + channel=1, + is_remote_frame=True, + ), + can.Message( + timestamp=2.520002, + arbitration_id=0x200, + is_extended_id=False, + is_rx=False, + channel=2, + is_remote_frame=True, + ), + can.Message( + timestamp=2.584921, + arbitration_id=0x300, + is_extended_id=False, + channel=3, + dlc=8, + is_remote_frame=True, + ), + ] + actual = self._read_log_file("test_CanRemoteMessage.asc") + self.assertMessagesEqual(actual, expected_messages) + + def test_can_fd_remote_message(self): + expected_messages = [ + can.Message( + timestamp=30.300981, + arbitration_id=0x50005, + channel=2, + dlc=5, + is_rx=False, + is_fd=True, + is_remote_frame=True, + error_state_indicator=True, + ) + ] + actual = self._read_log_file("test_CanFdRemoteMessage.asc") + self.assertMessagesEqual(actual, expected_messages) + + def test_can_fd_message(self): + expected_messages = [ + can.Message( + timestamp=30.005021, + arbitration_id=0x300, + is_extended_id=False, + channel=0, + dlc=8, + data=[0x11, 0xC2, 3, 4, 5, 6, 7, 8], + is_fd=True, + bitrate_switch=True, + ), + can.Message( + timestamp=30.005041, + arbitration_id=0x1C4D80A7, + channel=1, + dlc=8, + is_rx=False, + data=[0x12, 0xC2, 3, 4, 5, 6, 7, 8], + is_fd=True, + error_state_indicator=True, + ), + can.Message( + timestamp=30.005071, + arbitration_id=0x30A, + is_extended_id=False, + channel=2, + dlc=8, + data=[1, 2, 3, 4, 5, 6, 7, 8], + is_fd=True, + bitrate_switch=True, + error_state_indicator=True, + ), + ] + actual = self._read_log_file("test_CanFdMessage.asc") + self.assertMessagesEqual(actual, expected_messages) + + def test_can_fd_message_64(self): + expected_messages = [ + can.Message( + timestamp=30.506898, + arbitration_id=0x4EE, + is_extended_id=False, + channel=3, + dlc=0xF, + data=[0xA1, 2, 3, 4] + 59 * [0] + [0x64], + is_fd=True, + error_state_indicator=True, + ), + can.Message( + timestamp=31.506898, + arbitration_id=0x1C4D80A7, + channel=3, + dlc=0xF, + data=[0xB1, 2, 3, 4] + 59 * [0] + [0x64], + is_fd=True, + bitrate_switch=True, + ), + ] + actual = self._read_log_file("test_CanFdMessage64.asc") + self.assertMessagesEqual(actual, expected_messages) + + def test_can_and_canfd_error_frames(self): + expected_messages = [ + can.Message(timestamp=2.501000, channel=0, is_error_frame=True), + can.Message(timestamp=3.501000, channel=0, is_error_frame=True), + can.Message(timestamp=4.501000, channel=1, is_error_frame=True), + can.Message( + timestamp=30.806898, + channel=4, + is_rx=False, + is_error_frame=True, + is_fd=True, + ), + ] + actual = self._read_log_file("test_CanErrorFrames.asc") + self.assertMessagesEqual(actual, expected_messages) + class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader.