77Version 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
1111from enum import Enum
1212import io
1313import os
1414import logging
15- from typing import Generator , Optional , Union , TextIO , Callable , List
15+ from typing import Generator , Optional , Union , TextIO , Callable , List , Dict
1616
1717from ..message import Message
18- from ..util import channel2int
18+ from ..util import channel2int , len2dlc , dlc2len
1919from .generic import FileIOMessageWriter , MessageReader
2020from ..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
3641class 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
0 commit comments