Skip to content

Commit 740c50c

Browse files
twamzariiii9003
andauthored
Improve support for TRC files (#1530)
* Improve support for TRC files * Add support for Version 2.0 * Add support for $STARTTIME in Version 1.1 and Version 2.1 * Add support for $COLUMNS in Version 2.1 * Add type annotations * Fix mypy findings * remove assert --------- Co-authored-by: zariiii9003 <[email protected]>
1 parent fa5b133 commit 740c50c

File tree

3 files changed

+98
-34
lines changed

3 files changed

+98
-34
lines changed

can/io/trc.py

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
Version 1.1 will be implemented as it is most commonly used
88
""" # noqa
99

10-
from datetime import datetime, timedelta
10+
from datetime import datetime, timedelta, timezone
1111
from enum import Enum
1212
import io
1313
import os
1414
import logging
15-
from typing import Generator, Optional, Union, TextIO, Callable, List
15+
from typing import Generator, Optional, Union, TextIO, Callable, List, Dict
1616

1717
from ..message import Message
18-
from ..util import channel2int
18+
from ..util import channel2int, len2dlc, dlc2len
1919
from .generic import FileIOMessageWriter, MessageReader
2020
from ..typechecking import StringPathLike
2121

@@ -32,6 +32,11 @@ class TRCFileVersion(Enum):
3232
V2_0 = 200
3333
V2_1 = 201
3434

35+
def __ge__(self, other):
36+
if self.__class__ is other.__class__:
37+
return self.value >= other.value
38+
return NotImplemented
39+
3540

3641
class TRCReader(MessageReader):
3742
"""
@@ -51,6 +56,8 @@ def __init__(
5156
"""
5257
super().__init__(file, mode="r")
5358
self.file_version = TRCFileVersion.UNKNOWN
59+
self.start_time: Optional[datetime] = None
60+
self.columns: Dict[str, int] = {}
5461

5562
if not self.file:
5663
raise ValueError("The given file cannot be None")
@@ -67,17 +74,42 @@ def _extract_header(self):
6774
file_version = line.split("=")[1]
6875
if file_version == "1.1":
6976
self.file_version = TRCFileVersion.V1_1
77+
elif file_version == "2.0":
78+
self.file_version = TRCFileVersion.V2_0
7079
elif file_version == "2.1":
7180
self.file_version = TRCFileVersion.V2_1
7281
else:
7382
self.file_version = TRCFileVersion.UNKNOWN
7483
except IndexError:
7584
logger.debug("TRCReader: Failed to parse version")
85+
elif line.startswith(";$STARTTIME"):
86+
logger.debug("TRCReader: Found start time '%s'", line)
87+
try:
88+
self.start_time = datetime(
89+
1899, 12, 30, tzinfo=timezone.utc
90+
) + timedelta(days=float(line.split("=")[1]))
91+
except IndexError:
92+
logger.debug("TRCReader: Failed to parse start time")
93+
elif line.startswith(";$COLUMNS"):
94+
logger.debug("TRCReader: Found columns '%s'", line)
95+
try:
96+
columns = line.split("=")[1].split(",")
97+
self.columns = {column: columns.index(column) for column in columns}
98+
except IndexError:
99+
logger.debug("TRCReader: Failed to parse columns")
76100
elif line.startswith(";"):
77101
continue
78102
else:
79103
break
80104

105+
if self.file_version >= TRCFileVersion.V1_1:
106+
if self.start_time is None:
107+
raise ValueError("File has no start time information")
108+
109+
if self.file_version >= TRCFileVersion.V2_0:
110+
if not self.columns:
111+
raise ValueError("File has no column information")
112+
81113
if self.file_version == TRCFileVersion.UNKNOWN:
82114
logger.info(
83115
"TRCReader: No file version was found, so version 1.0 is assumed"
@@ -87,8 +119,8 @@ def _extract_header(self):
87119
self._parse_cols = self._parse_msg_V1_0
88120
elif self.file_version == TRCFileVersion.V1_1:
89121
self._parse_cols = self._parse_cols_V1_1
90-
elif self.file_version == TRCFileVersion.V2_1:
91-
self._parse_cols = self._parse_cols_V2_1
122+
elif self.file_version in [TRCFileVersion.V2_0, TRCFileVersion.V2_1]:
123+
self._parse_cols = self._parse_cols_V2_x
92124
else:
93125
raise NotImplementedError("File version not fully implemented for reading")
94126

@@ -113,7 +145,12 @@ def _parse_msg_V1_1(self, cols: List[str]) -> Optional[Message]:
113145
arbit_id = cols[3]
114146

115147
msg = Message()
116-
msg.timestamp = float(cols[1]) / 1000
148+
if isinstance(self.start_time, datetime):
149+
msg.timestamp = (
150+
self.start_time + timedelta(milliseconds=float(cols[1]))
151+
).timestamp()
152+
else:
153+
msg.timestamp = float(cols[1]) / 1000
117154
msg.arbitration_id = int(arbit_id, 16)
118155
msg.is_extended_id = len(arbit_id) > 4
119156
msg.channel = 1
@@ -122,15 +159,38 @@ def _parse_msg_V1_1(self, cols: List[str]) -> Optional[Message]:
122159
msg.is_rx = cols[2] == "Rx"
123160
return msg
124161

125-
def _parse_msg_V2_1(self, cols: List[str]) -> Optional[Message]:
162+
def _parse_msg_V2_x(self, cols: List[str]) -> Optional[Message]:
163+
type_ = cols[self.columns["T"]]
164+
bus = self.columns.get("B", None)
165+
166+
if "l" in self.columns:
167+
length = int(cols[self.columns["l"]])
168+
dlc = len2dlc(length)
169+
elif "L" in self.columns:
170+
dlc = int(cols[self.columns["L"]])
171+
length = dlc2len(dlc)
172+
else:
173+
raise ValueError("No length/dlc columns present.")
174+
126175
msg = Message()
127-
msg.timestamp = float(cols[1]) / 1000
128-
msg.arbitration_id = int(cols[4], 16)
129-
msg.is_extended_id = len(cols[4]) > 4
130-
msg.channel = int(cols[3])
131-
msg.dlc = int(cols[7])
132-
msg.data = bytearray([int(cols[i + 8], 16) for i in range(msg.dlc)])
133-
msg.is_rx = cols[5] == "Rx"
176+
if isinstance(self.start_time, datetime):
177+
msg.timestamp = (
178+
self.start_time + timedelta(milliseconds=float(cols[self.columns["O"]]))
179+
).timestamp()
180+
else:
181+
msg.timestamp = float(cols[1]) / 1000
182+
msg.arbitration_id = int(cols[self.columns["I"]], 16)
183+
msg.is_extended_id = len(cols[self.columns["I"]]) > 4
184+
msg.channel = int(cols[bus]) if bus is not None else 1
185+
msg.dlc = dlc
186+
msg.data = bytearray(
187+
[int(cols[i + self.columns["D"]], 16) for i in range(length)]
188+
)
189+
msg.is_rx = cols[self.columns["d"]] == "Rx"
190+
msg.is_fd = type_ in ["FD", "FB", "FE", "BI"]
191+
msg.bitrate_switch = type_ in ["FB", " FE"]
192+
msg.error_state_indicator = type_ in ["FE", "BI"]
193+
134194
return msg
135195

136196
def _parse_cols_V1_1(self, cols: List[str]) -> Optional[Message]:
@@ -141,10 +201,10 @@ def _parse_cols_V1_1(self, cols: List[str]) -> Optional[Message]:
141201
logger.info("TRCReader: Unsupported type '%s'", dtype)
142202
return None
143203

144-
def _parse_cols_V2_1(self, cols: List[str]) -> Optional[Message]:
145-
dtype = cols[2]
146-
if dtype == "DT":
147-
return self._parse_msg_V2_1(cols)
204+
def _parse_cols_V2_x(self, cols: List[str]) -> Optional[Message]:
205+
dtype = cols[self.columns["T"]]
206+
if dtype in ["DT", "FD", "FB"]:
207+
return self._parse_msg_V2_x(cols)
148208
else:
149209
logger.info("TRCReader: Unsupported type '%s'", dtype)
150210
return None
@@ -228,7 +288,7 @@ def __init__(
228288
self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0
229289
self._format_message = self._format_message_init
230290

231-
def _write_header_V1_0(self, start_time: timedelta) -> None:
291+
def _write_header_V1_0(self, start_time: datetime) -> None:
232292
lines = [
233293
";##########################################################################",
234294
f"; {self.filepath}",
@@ -249,13 +309,11 @@ def _write_header_V1_0(self, start_time: timedelta) -> None:
249309
]
250310
self.file.writelines(line + "\n" for line in lines)
251311

252-
def _write_header_V2_1(self, header_time: timedelta, start_time: datetime) -> None:
253-
milliseconds = int(
254-
(header_time.seconds * 1000) + (header_time.microseconds / 1000)
255-
)
312+
def _write_header_V2_1(self, start_time: datetime) -> None:
313+
header_time = start_time - datetime(year=1899, month=12, day=30)
256314
lines = [
257315
";$FILEVERSION=2.1",
258-
f";$STARTTIME={header_time.days}.{milliseconds}",
316+
f";$STARTTIME={header_time/timedelta(days=1)}",
259317
";$COLUMNS=N,O,T,B,I,d,R,L,D",
260318
";",
261319
f"; {self.filepath}",
@@ -308,14 +366,12 @@ def _format_message_init(self, msg, channel):
308366

309367
def write_header(self, timestamp: float) -> None:
310368
# write start of file header
311-
ref_time = datetime(year=1899, month=12, day=30)
312-
start_time = datetime.now() + timedelta(seconds=timestamp)
313-
header_time = start_time - ref_time
369+
start_time = datetime.utcfromtimestamp(timestamp)
314370

315371
if self.file_version == TRCFileVersion.V1_0:
316-
self._write_header_V1_0(header_time)
372+
self._write_header_V1_0(start_time)
317373
elif self.file_version == TRCFileVersion.V2_1:
318-
self._write_header_V2_1(header_time, start_time)
374+
self._write_header_V2_1(start_time)
319375
else:
320376
raise NotImplementedError("File format is not supported")
321377
self.header_written = True

test/data/test_CanMessage.trc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
;$FILEVERSION=2.1
2-
;$STARTTIME=0
2+
;$STARTTIME=43008.920986006946
33
;$COLUMNS=N,O,T,B,I,d,R,L,D
44
;
55
; C:\Users\User\Desktop\python-can\test\data\test_CanMessage.trc

test/logformats_test.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -810,9 +810,10 @@ class TestTrcFileFormatGen(TestTrcFileFormatBase):
810810
"""Generic tests for can.TRCWriter and can.TRCReader with different file versions"""
811811

812812
def test_can_message(self):
813+
start_time = 1506809173.191 # 30.09.2017 22:06:13.191.000 as timestamp
813814
expected_messages = [
814815
can.Message(
815-
timestamp=2.5010,
816+
timestamp=start_time + 2.5010,
816817
arbitration_id=0xC8,
817818
is_extended_id=False,
818819
is_rx=False,
@@ -821,7 +822,7 @@ def test_can_message(self):
821822
data=[9, 8, 7, 6, 5, 4, 3, 2],
822823
),
823824
can.Message(
824-
timestamp=17.876708,
825+
timestamp=start_time + 17.876708,
825826
arbitration_id=0x6F9,
826827
is_extended_id=False,
827828
channel=0,
@@ -841,10 +842,17 @@ def test_can_message(self):
841842
)
842843
def test_can_message_versions(self, name, filename, is_rx_support):
843844
with self.subTest(name):
845+
if name == "V1_0":
846+
# Version 1.0 does not support start time
847+
start_time = 0
848+
else:
849+
start_time = (
850+
1639837687.062001 # 18.12.2021 14:28:07.062.001 as timestamp
851+
)
844852

845853
def msg_std(timestamp):
846854
msg = can.Message(
847-
timestamp=timestamp,
855+
timestamp=timestamp + start_time,
848856
arbitration_id=0x000,
849857
is_extended_id=False,
850858
channel=1,
@@ -857,7 +865,7 @@ def msg_std(timestamp):
857865

858866
def msg_ext(timestamp):
859867
msg = can.Message(
860-
timestamp=timestamp,
868+
timestamp=timestamp + start_time,
861869
arbitration_id=0x100,
862870
is_extended_id=True,
863871
channel=1,

0 commit comments

Comments
 (0)