|
| 1 | +""" |
| 2 | +Implements support for BLF (Binary Logging Format) which is a proprietary |
| 3 | +CAN log format from Vector Informatik GmbH. |
| 4 | +
|
| 5 | +No official specification of the binary logging format is available. |
| 6 | +This implementation is based on Toby Lorenz' C++ library "Vector BLF" which is |
| 7 | +licenced under GPLv3. https://bitbucket.org/tobylorenz/vector_blf. |
| 8 | +The file starts with a header. The rest is one or more "log containers" |
| 9 | +which consists of a header and some zlib compressed data, usually up to 128 kB |
| 10 | +of uncompressed data each. This data contains the actual CAN messages and other |
| 11 | +objects types. |
| 12 | +""" |
| 13 | +import struct |
| 14 | +import zlib |
| 15 | +import datetime |
| 16 | +import time |
| 17 | + |
| 18 | +from can.message import Message |
| 19 | +from can.CAN import Listener |
| 20 | + |
| 21 | + |
| 22 | +# 0 = unknown, 2 = CANoe |
| 23 | +APPLICATION_ID = 5 |
| 24 | + |
| 25 | +# Header must be 144 bytes in total |
| 26 | +# signature ("LOGG"), header size, |
| 27 | +# application ID, application major, application minor, application build, |
| 28 | +# bin log major, bin log minor, bin log build, bin log patch, |
| 29 | +# file size, uncompressed size, count of objects, count of objects read, |
| 30 | +# time start (SYSTEMTIME), time stop (SYSTEMTIME) |
| 31 | +FILE_HEADER_STRUCT = struct.Struct("<4sLBBBBBBBBQQLL8H8H72x") |
| 32 | + |
| 33 | +# signature ("LOBJ"), header size, header version (1), object size, object type, |
| 34 | +# flags, object version, size uncompressed or timestamp |
| 35 | +OBJ_HEADER_STRUCT = struct.Struct("<4sHHLLL2xHQ") |
| 36 | + |
| 37 | +# channel, flags, dlc, arbitration id, data |
| 38 | +CAN_MSG_STRUCT = struct.Struct("<HBBL8s") |
| 39 | + |
| 40 | +# channel, length |
| 41 | +CAN_ERROR_STRUCT = struct.Struct("<HH4x") |
| 42 | + |
| 43 | +# commented event type, foreground color, background color, relocatable, |
| 44 | +# group name length, marker name length, description length |
| 45 | +GLOBAL_MARKER_STRUCT = struct.Struct("<LLL3xBLLL12x") |
| 46 | + |
| 47 | + |
| 48 | +CAN_MESSAGE = 1 |
| 49 | +CAN_ERROR = 2 |
| 50 | +LOG_CONTAINER = 10 |
| 51 | +GLOBAL_MARKER = 96 |
| 52 | + |
| 53 | +CAN_MSG_EXT = 0x80000000 |
| 54 | +REMOTE_FLAG = 0x80 |
| 55 | + |
| 56 | + |
| 57 | +def timestamp_to_systemtime(timestamp): |
| 58 | + if timestamp is None or timestamp < 631152000: |
| 59 | + # Probably not a Unix timestamp |
| 60 | + return (0, 0, 0, 0, 0, 0, 0, 0) |
| 61 | + t = datetime.datetime.fromtimestamp(timestamp) |
| 62 | + return (t.year, t.month, t.isoweekday() % 7, t.day, |
| 63 | + t.hour, t.minute, t.second, int(round(t.microsecond / 1000.0))) |
| 64 | + |
| 65 | + |
| 66 | +def systemtime_to_timestamp(systemtime): |
| 67 | + try: |
| 68 | + t = datetime.datetime( |
| 69 | + systemtime[0], systemtime[1], systemtime[3], |
| 70 | + systemtime[4], systemtime[5], systemtime[6], systemtime[7] * 1000) |
| 71 | + return time.mktime(t.timetuple()) + systemtime[7] / 1000.0 |
| 72 | + except ValueError: |
| 73 | + return 0 |
| 74 | + |
| 75 | + |
| 76 | +class BLFReader(object): |
| 77 | + """ |
| 78 | + Iterator of CAN messages from a Binary Logging File. |
| 79 | +
|
| 80 | + Only CAN messages and error frames are supported. Other object types are |
| 81 | + silently ignored. |
| 82 | + """ |
| 83 | + |
| 84 | + def __init__(self, filename): |
| 85 | + self.fp = open(filename, "rb") |
| 86 | + data = self.fp.read(FILE_HEADER_STRUCT.size) |
| 87 | + header = FILE_HEADER_STRUCT.unpack(data) |
| 88 | + #print(header) |
| 89 | + assert header[0] == b"LOGG", "Unknown file format" |
| 90 | + self.start_timestamp = systemtime_to_timestamp(header[14:22]) |
| 91 | + |
| 92 | + def __iter__(self): |
| 93 | + tail = b"" |
| 94 | + while True: |
| 95 | + data = self.fp.read(OBJ_HEADER_STRUCT.size) |
| 96 | + if not data: |
| 97 | + # EOF |
| 98 | + break |
| 99 | + header = OBJ_HEADER_STRUCT.unpack(data) |
| 100 | + #print(header) |
| 101 | + assert header[0] == b"LOBJ", "Parse error" |
| 102 | + obj_type = header[4] |
| 103 | + obj_data_size = header[3] - OBJ_HEADER_STRUCT.size |
| 104 | + obj_data = self.fp.read(obj_data_size) |
| 105 | + # Read padding bytes |
| 106 | + self.fp.read(obj_data_size % 4) |
| 107 | + if obj_type == LOG_CONTAINER: |
| 108 | + uncompressed_size = header[7] |
| 109 | + data = zlib.decompress(obj_data, 15, uncompressed_size) |
| 110 | + if tail: |
| 111 | + data = tail + data |
| 112 | + pos = 0 |
| 113 | + while pos + OBJ_HEADER_STRUCT.size < len(data): |
| 114 | + header = OBJ_HEADER_STRUCT.unpack( |
| 115 | + data[pos:pos + OBJ_HEADER_STRUCT.size]) |
| 116 | + #print(header) |
| 117 | + assert header[0] == b"LOBJ", "Parse error" |
| 118 | + obj_size = header[3] |
| 119 | + if pos + obj_size > len(data): |
| 120 | + # Object continues in next log container |
| 121 | + break |
| 122 | + obj_data = data[pos + OBJ_HEADER_STRUCT.size:pos + obj_size] |
| 123 | + obj_type = header[4] |
| 124 | + timestamp = header[7] / 1000000000.0 + self.start_timestamp |
| 125 | + if obj_type == CAN_MESSAGE: |
| 126 | + (channel, flags, dlc, can_id, |
| 127 | + can_data) = CAN_MSG_STRUCT.unpack(obj_data) |
| 128 | + msg = Message(timestamp=timestamp, |
| 129 | + arbitration_id=can_id & 0x1FFFFFFF, |
| 130 | + extended_id=bool(can_id & CAN_MSG_EXT), |
| 131 | + is_remote_frame=bool(flags & REMOTE_FLAG), |
| 132 | + dlc=dlc, |
| 133 | + data=can_data[:dlc]) |
| 134 | + msg.channel = channel |
| 135 | + yield msg |
| 136 | + elif obj_type == CAN_ERROR: |
| 137 | + channel, length = CAN_ERROR_STRUCT.unpack(obj_data) |
| 138 | + msg = Message(timestamp=timestamp, is_error_frame=True) |
| 139 | + msg.channel = channel |
| 140 | + yield msg |
| 141 | + pos += obj_size |
| 142 | + # Add padding bytes |
| 143 | + pos += obj_size % 4 |
| 144 | + # Save remaing data that could not be processed |
| 145 | + tail = data[pos:] |
| 146 | + self.fp.close() |
| 147 | + |
| 148 | + |
| 149 | +class BLFWriter(Listener): |
| 150 | + """ |
| 151 | + Logs CAN data to a Binary Logging File compatible with Vector's tools. |
| 152 | + """ |
| 153 | + |
| 154 | + #: Max log container size of uncompressed data |
| 155 | + MAX_CACHE_SIZE = 0x20000 |
| 156 | + |
| 157 | + #: ZLIB compression level |
| 158 | + COMPRESSION_LEVEL = 7 |
| 159 | + |
| 160 | + def __init__(self, filename, channel=1): |
| 161 | + self.fp = open(filename, "wb") |
| 162 | + self.channel = channel |
| 163 | + # Header will be written after log is done |
| 164 | + self.fp.write(b"\x00" * FILE_HEADER_STRUCT.size) |
| 165 | + self.cache = [] |
| 166 | + self.cache_size = 0 |
| 167 | + self.count_of_objects = 0 |
| 168 | + self.uncompressed_size = FILE_HEADER_STRUCT.size |
| 169 | + self.start_timestamp = None |
| 170 | + self.stop_timestamp = None |
| 171 | + |
| 172 | + def on_message_received(self, msg): |
| 173 | + if self.start_timestamp is None: |
| 174 | + self.start_timestamp = msg.timestamp |
| 175 | + self.stop_timestamp = msg.timestamp |
| 176 | + timestamp = int((msg.timestamp - self.start_timestamp) * 1000000000) |
| 177 | + if not msg.is_error_frame: |
| 178 | + obj_size = OBJ_HEADER_STRUCT.size + CAN_MSG_STRUCT.size |
| 179 | + header = OBJ_HEADER_STRUCT.pack( |
| 180 | + b"LOBJ", OBJ_HEADER_STRUCT.size, 1, obj_size, CAN_MESSAGE, |
| 181 | + 2, 0, timestamp) |
| 182 | + flags = REMOTE_FLAG if msg.is_remote_frame else 0 |
| 183 | + arb_id = msg.arbitration_id |
| 184 | + if msg.id_type: |
| 185 | + arb_id |= CAN_MSG_EXT |
| 186 | + data = CAN_MSG_STRUCT.pack(self.channel, flags, msg.dlc, arb_id, |
| 187 | + bytes(msg.data)) |
| 188 | + else: |
| 189 | + obj_size = OBJ_HEADER_STRUCT.size + CAN_ERROR_STRUCT.size |
| 190 | + header = OBJ_HEADER_STRUCT.pack( |
| 191 | + b"LOBJ", OBJ_HEADER_STRUCT.size, 1, obj_size, CAN_ERROR, |
| 192 | + 2, 0, timestamp) |
| 193 | + data = CAN_ERROR_STRUCT.pack(self.channel, 0) |
| 194 | + self._add_data(header + data) |
| 195 | + |
| 196 | + def log_event(self, text, timestamp=None): |
| 197 | + """Add an arbitrary message to the log file as a global marker. |
| 198 | +
|
| 199 | + :param str text: |
| 200 | + The group name of the marker. |
| 201 | + :param float timestamp: |
| 202 | + Absolute timestamp in Unix timestamp format. If not given, the |
| 203 | + marker will be placed along the last message. |
| 204 | + """ |
| 205 | + if timestamp is None: |
| 206 | + timestamp = self.stop_timestamp |
| 207 | + if self.start_timestamp is None: |
| 208 | + self.start_timestamp = timestamp |
| 209 | + self.stop_timestamp = timestamp |
| 210 | + try: |
| 211 | + # Only works on Windows |
| 212 | + text = text.encode("mbcs") |
| 213 | + except LookupError: |
| 214 | + text = text.encode("ascii") |
| 215 | + timestamp = int((timestamp - self.start_timestamp) * 1000000000) |
| 216 | + comment = b"Added by python-can" |
| 217 | + marker = b"python-can" |
| 218 | + obj_size = (OBJ_HEADER_STRUCT.size + GLOBAL_MARKER_STRUCT.size + |
| 219 | + len(text) + len(marker) + len(comment)) |
| 220 | + header = OBJ_HEADER_STRUCT.pack( |
| 221 | + b"LOBJ", OBJ_HEADER_STRUCT.size, 1, obj_size, GLOBAL_MARKER, |
| 222 | + 2, 0, timestamp) |
| 223 | + data = GLOBAL_MARKER_STRUCT.pack( |
| 224 | + 0, 0xFFFFFF, 0xFF3300, 0, len(text), len(marker), len(comment)) |
| 225 | + self._add_data(header + data + text + marker + comment) |
| 226 | + |
| 227 | + def _add_data(self, data): |
| 228 | + if len(data) % 4: |
| 229 | + data = data + b"\x00" * (len(data) % 4) |
| 230 | + self.cache.append(data) |
| 231 | + self.cache_size += len(data) |
| 232 | + self.count_of_objects += 1 |
| 233 | + if self.cache_size >= self.MAX_CACHE_SIZE: |
| 234 | + self._flush() |
| 235 | + |
| 236 | + def _flush(self): |
| 237 | + """Compresses and writes data in the cache to file.""" |
| 238 | + cache = b"".join(self.cache) |
| 239 | + if not cache: |
| 240 | + # Nothing to write |
| 241 | + return |
| 242 | + uncompressed_data = cache[:self.MAX_CACHE_SIZE] |
| 243 | + # Save data that comes after max size to next round |
| 244 | + tail = cache[self.MAX_CACHE_SIZE:] |
| 245 | + self.cache = [tail] |
| 246 | + self.cache_size = len(tail) |
| 247 | + compressed_data = zlib.compress(uncompressed_data, |
| 248 | + self.COMPRESSION_LEVEL) |
| 249 | + obj_size = OBJ_HEADER_STRUCT.size + len(compressed_data) |
| 250 | + header = OBJ_HEADER_STRUCT.pack( |
| 251 | + b"LOBJ", 16, 1, obj_size, LOG_CONTAINER, 2, 0, len(uncompressed_data)) |
| 252 | + self.fp.write(header) |
| 253 | + self.fp.write(compressed_data) |
| 254 | + # Write padding bytes |
| 255 | + self.fp.write(b"\x00" * (obj_size % 4)) |
| 256 | + self.uncompressed_size += len(uncompressed_data) + OBJ_HEADER_STRUCT.size |
| 257 | + |
| 258 | + def stop(self): |
| 259 | + """Stops logging and closes the file.""" |
| 260 | + self._flush() |
| 261 | + filesize = self.fp.tell() |
| 262 | + self.fp.close() |
| 263 | + |
| 264 | + # Write header in the beginning of the file |
| 265 | + header = [b"LOGG", FILE_HEADER_STRUCT.size, |
| 266 | + APPLICATION_ID, 0, 0, 0, 2, 6, 8, 1] |
| 267 | + # The meaning of "count of objects read" is unknown |
| 268 | + header.extend([filesize, self.uncompressed_size, |
| 269 | + self.count_of_objects, 0]) |
| 270 | + header.extend(timestamp_to_systemtime(self.start_timestamp)) |
| 271 | + header.extend(timestamp_to_systemtime(self.stop_timestamp)) |
| 272 | + with open(self.fp.name, "r+b") as f: |
| 273 | + f.write(FILE_HEADER_STRUCT.pack(*header)) |
0 commit comments