From 783b90206e1f2ebf32fbeed86f680f725f9ecf75 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Fri, 11 Sep 2026 09:03:05 +0200 Subject: [PATCH 1/3] blf: interpret object timestamps as signed 64-bit values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change BLF object header timestamp fields from uint64 (Q) to int64 (q) for both V1 and V2 object headers. Observed CANape-generated BLF files containing messages before the recording start timestamp. When interpreted as unsigned values, these timestamps wrap around and appear approximately 2^64 ns in the future, resulting in invalid timestamps (e.g. year 2611) and NaT values in pandas. Using signed 64-bit timestamps correctly preserves negative recording-relative offsets while still providing a usable range of approximately ±292 years at nanosecond resolution. Root cause:   A timestamp of -60 s was decoded as   2^64 ns - 60 s   which produced timestamps approximately 584 years in the future. The resulting values exceeded the valid pandas datetime range and appeared as NaT. --- can/io/blf.py | 4 ++-- doc/changelog.d/+blf_prerecording_timestamps.fixed.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/+blf_prerecording_timestamps.fixed.rst diff --git a/can/io/blf.py b/can/io/blf.py index 77bd02fae..26cae7de5 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -49,10 +49,10 @@ class BLFParseError(Exception): OBJ_HEADER_BASE_STRUCT = struct.Struct("<4sHHLL") # flags, client index, object version, timestamp -OBJ_HEADER_V1_STRUCT = struct.Struct(" Date: Thu, 8 Oct 2026 23:56:49 +0200 Subject: [PATCH 2/3] blf: add regression tests for pre-recording timestamps Add regression tests covering messages that occur before the recording start timestamp. The first test performs a BLFReader -> BLFWriter -> BLFReader roundtrip using the existing BLF files from the test suite. Besides verifying timestamp preservation, this provides a convenient framework for reproducing and validating real-world BLF files when investigating format compatibility issues. The second test verifies that messages with negative recording-relative timestamps survive a write/read cycle without modification. These tests reproduce the failure modes seen working with Vector CANape blf recordings which are fixed by the previous and following change. --- test/logformats_test.py | 33 +++++++++++++++++++++++++++++++++ test/message_helper.py | 18 +++++++++++++----- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 42ed29b13..aca9ba342 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -908,6 +908,39 @@ def test_can_fd_message_64(self): self.assertMessagesEqual(actual, [expected] * 2) self.assertEqual(actual[0].channel, expected.channel) + def test_readwrite_cycle(self): + data_folder = os.path.join(os.path.dirname(__file__), "data") + dont_test_files = ["test_CanErrorFrameExt.blf"] + blf_test_files = [ f for f in os.listdir(data_folder) if f.endswith(".blf") and f not in dont_test_files ] + for src in blf_test_files: + with can.BLFReader(os.path.join(data_folder, src)) as reader: + msgs = list(reader) + with can.BLFWriter(self.test_file_name, timestamps_format="absolute") as writer: + writer.start_timestamp = reader.start_timestamp + for m in msgs: + writer.on_message_received(m) + + with can.BLFReader(self.test_file_name) as verification_reader: + verification_msgs = list(verification_reader) + self.assertMessagesEqual(msgs, verification_msgs, msg="file: " + src) + + def test_pre_recording_timestamp(self): + t0 = 716868000.0 + msgs_absolute = [ + can.Message(timestamp=t0 - 0.1, arbitration_id=0x123, data=b"\x01"), + can.Message(timestamp=t0, arbitration_id=0x456, data=b"\x02"), + can.Message(timestamp=t0 + 0.1, arbitration_id=0x789, data=b"\x03"), + ] + + with can.BLFWriter(self.test_file_name) as writer: + writer.start_timestamp = t0 + for m in msgs_absolute: + writer.on_message_received(m) + with can.BLFReader(self.test_file_name) as reader: + print("reader.start_timestamp:", reader.start_timestamp) + resulting_msgs = list(reader) + self.assertMessagesEqual(resulting_msgs, msgs_absolute) + def test_can_error_frame_ext(self): expected = can.Message( timestamp=2459565876.494607, diff --git a/test/message_helper.py b/test/message_helper.py index d10e9195f..a431062c7 100644 --- a/test/message_helper.py +++ b/test/message_helper.py @@ -21,7 +21,7 @@ def __init__(self, allowed_timestamp_delta=0.0, preserves_channel=True): self.allowed_timestamp_delta = allowed_timestamp_delta self.preserves_channel = preserves_channel - def assertMessageEqual(self, message_1, message_2): + def assertMessageEqual(self, message_1, message_2, msg=None): """ Checks that two messages are equal, according to the given rules. """ @@ -31,18 +31,26 @@ def assertMessageEqual(self, message_1, message_2): check_channel=self.preserves_channel, timestamp_delta=self.allowed_timestamp_delta, ): + fail_message = f"messages are unequal: \n{message_1}\n{message_2}" + if msg: + fail_message = f"{msg}: {fail_message}" + print(f"Assertion message: {msg}") print(f"Comparing: message 1: {message_1!r}") print(f" message 2: {message_2!r}") - self.fail(f"messages are unequal: \n{message_1}\n{message_2}") + self.fail(fail_message) - def assertMessagesEqual(self, messages_1, messages_2): + def assertMessagesEqual(self, messages_1, messages_2, msg=None): """ Checks the order and content of the individual messages pairwise. Raises an error if the lengths of the sequences are not equal. """ + assert_msg = "the number of messages differs" + if msg: + assert_msg = f"{assert_msg}2:2 {msg}" + self.assertEqual( - len(messages_1), len(messages_2), "the number of messages differs" + len(messages_1), len(messages_2), assert_msg ) for message_1, message_2 in zip(messages_1, messages_2): - self.assertMessageEqual(message_1, message_2) + self.assertMessageEqual(message_1, message_2, msg=msg) From 9b00490b1c796a7ae42cef6d04c228dd1e4b5697 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 8 Oct 2026 23:57:29 +0200 Subject: [PATCH 3/3] blf: allow writing negative timestamp offsets BLFWriter currently clamps negative recording-relative timestamps to zero before writing them to the BLF stream. This breaks BLF files containing messages that occur before the recording start timestamp. A practical example is Vector CANape recordings with a configured pre-recording buffer. When extracting a single CAN channel from such a measurement, the pre-recording messages have negative offsets relative to the BLF header start timestamp. Clamping these offsets to zero loses information and prevents a lossless BLFReader -> BLFWriter roundtrip. Preserving the original header start timestamp is desirable, as it provides a stable recording reference and can be used to identify or group related BLF fragments originating from a split measurement. Write the signed timestamp value unchanged instead of clamping it to zero. --- can/io/blf.py | 2 +- test/logformats_test.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 26cae7de5..f90c3aa10 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -546,7 +546,7 @@ def _add_object( base_header = OBJ_HEADER_BASE_STRUCT.pack( b"LOBJ", header_size, 1, obj_size, obj_type ) - obj_header = OBJ_HEADER_V1_STRUCT.pack(TIME_ONE_NANS, 0, 0, max(timestamp, 0)) + obj_header = OBJ_HEADER_V1_STRUCT.pack(TIME_ONE_NANS, 0, 0, timestamp) self._buffer.append(base_header) self._buffer.append(obj_header) diff --git a/test/logformats_test.py b/test/logformats_test.py index aca9ba342..86c369515 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -24,10 +24,9 @@ from pathlib import Path from unittest.mock import patch -from parameterized import parameterized - import can.io from can.io import asc, blf +from parameterized import parameterized from .data.example_data import ( TEST_COMMENTS,