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/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. diff --git a/test/logformats_test.py b/test/logformats_test.py index f8a8de91d..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 @@ -680,6 +681,47 @@ 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( [ (