From 5f2824faf05078080c19a87efff6c00aa5337a04 Mon Sep 17 00:00:00 2001 From: Ju-Daeng-E Date: Tue, 24 Feb 2026 12:31:25 +0100 Subject: [PATCH 1/4] feat(asc): add timestamps_format parameter to ASCWriter Allow callers to choose between 'absolute' (default, existing behaviour) and 'relative' when creating an ASC log file. The value is written into the 'base hex timestamps ...' header line so that other tools (CANalyzer, CANoe, etc.) can interpret the file correctly. Closes #2022 --- can/io/asc.py | 18 ++++++++++++++++- test/logformats_test.py | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/can/io/asc.py b/can/io/asc.py index fcf8fc5e4..ad5538899 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -358,6 +358,7 @@ def __init__( self, file: StringPathLike | TextIO, channel: int = 1, + timestamps_format: str = "absolute", **kwargs: Any, ) -> None: """ @@ -366,7 +367,21 @@ def __init__( write mode, not binary write mode. :param channel: a default channel to use when the message does not have a channel set + :param timestamps_format: the format of timestamps in the header. + Use ``"absolute"`` (default) so that readers can recover + the original wall-clock timestamps by combining the + per-message offset with the trigger-block start time. + Use ``"relative"`` when only the elapsed time from the + start of the recording matters and no absolute time + recovery is needed. + :raises ValueError: if *timestamps_format* is not ``"absolute"`` or + ``"relative"`` """ + if timestamps_format not in ("absolute", "relative"): + raise ValueError( + f"timestamps_format must be 'absolute' or 'relative', " + f"got {timestamps_format!r}" + ) if kwargs.get("append", False): raise ValueError( f"{self.__class__.__name__} is currently not equipped to " @@ -375,11 +390,12 @@ def __init__( super().__init__(file, mode="w") self.channel = channel + self.timestamps_format = timestamps_format # write start of file header start_time = self._format_header_datetime(datetime.now()) self.file.write(f"date {start_time}\n") - self.file.write("base hex timestamps absolute\n") + self.file.write(f"base hex timestamps {self.timestamps_format}\n") self.file.write("internal events logged\n") # the last part is written with the timestamp of the first message diff --git a/test/logformats_test.py b/test/logformats_test.py index f8a8de91d..82aaff03e 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -680,6 +680,49 @@ def test_write(self): self.assertEqual(expected_file.read_text(), actual_file.read_text()) + def test_write_timestamps_format_default_is_absolute(self): + """ASCWriter should write 'timestamps absolute' in the header by default.""" + with can.ASCWriter(self.test_file_name) as writer: + pass + + content = Path(self.test_file_name).read_text() + self.assertIn("timestamps absolute", content) + + def test_write_timestamps_format_relative(self): + """ASCWriter should write 'timestamps relative' when requested.""" + with can.ASCWriter(self.test_file_name, timestamps_format="relative") as writer: + pass + + content = Path(self.test_file_name).read_text() + self.assertIn("timestamps relative", content) + self.assertNotIn("timestamps absolute", content) + + def test_write_timestamps_format_invalid(self): + """ASCWriter should raise ValueError for an unsupported timestamps_format.""" + with self.assertRaises(ValueError): + can.ASCWriter(self.test_file_name, timestamps_format="unix") + + def test_write_relative_timestamp_roundtrip(self): + """Messages written with relative format round-trip with relative timestamps.""" + msgs = [ + can.Message(timestamp=100.0, arbitration_id=0x1, data=b"\x01"), + can.Message(timestamp=100.5, arbitration_id=0x2, data=b"\x02"), + ] + + with can.ASCWriter( + self.test_file_name, timestamps_format="relative" + ) as writer: + for m in msgs: + writer.on_message_received(m) + + with can.ASCReader(self.test_file_name, relative_timestamp=True) as reader: + result = list(reader) + + self.assertEqual(len(result), len(msgs)) + # With relative_timestamp=True timestamps are offsets from the first message + self.assertAlmostEqual(result[0].timestamp, 0.0, places=5) + self.assertAlmostEqual(result[1].timestamp, 0.5, places=5) + @parameterized.expand( [ ( From 5bb3e899ae387417cf1c1b5c5566f2d71952e905 Mon Sep 17 00:00:00 2001 From: Ju-Daeng-E Date: Tue, 24 Feb 2026 12:36:56 +0100 Subject: [PATCH 2/4] docs: add changelog fragment for #2022 --- doc/changelog.d/2022.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/2022.added.md diff --git a/doc/changelog.d/2022.added.md b/doc/changelog.d/2022.added.md new file mode 100644 index 000000000..4a79c2d4e --- /dev/null +++ b/doc/changelog.d/2022.added.md @@ -0,0 +1 @@ +Added `timestamps_format` parameter to `ASCWriter` to allow writing `relative` or `absolute` timestamps in the ASC file header. From 0357eb4d1cbf6d7ea41dd191f22dfc5695a428bc Mon Sep 17 00:00:00 2001 From: Ju-Daeng-E Date: Tue, 24 Feb 2026 13:25:26 +0100 Subject: [PATCH 3/4] style: apply black formatting to logformats_test.py --- test/logformats_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 82aaff03e..de9dcf548 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -11,6 +11,7 @@ TODO: correctly set preserves_channel and adds_default_channel """ + import locale import logging import os @@ -709,9 +710,7 @@ def test_write_relative_timestamp_roundtrip(self): can.Message(timestamp=100.5, arbitration_id=0x2, data=b"\x02"), ] - with can.ASCWriter( - self.test_file_name, timestamps_format="relative" - ) as writer: + with can.ASCWriter(self.test_file_name, timestamps_format="relative") as writer: for m in msgs: writer.on_message_received(m) From 0ec90d78940dfaf56cc4d12e10a9dd878f6ff82d Mon Sep 17 00:00:00 2001 From: Ju-Daeng-E Date: Tue, 24 Feb 2026 15:30:06 +0100 Subject: [PATCH 4/4] ci: trigger re-run