Skip to content

Commit 0ec79a4

Browse files
committed
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
1 parent 74201b9 commit 0ec79a4

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

can/io/asc.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import re
1111
from collections.abc import Generator
1212
from datetime import datetime, timezone, tzinfo
13-
from typing import Any, Final, TextIO
13+
from typing import Any, Final, Literal, TextIO
1414

1515
from ..message import Message
1616
from ..typechecking import StringPathLike
@@ -372,6 +372,7 @@ def __init__(
372372
file: StringPathLike | TextIO,
373373
channel: int = 1,
374374
tz: tzinfo | None = _LOCAL_TZ,
375+
timestamps_format: Literal["absolute", "relative"] = "absolute",
375376
**kwargs: Any,
376377
) -> None:
377378
"""
@@ -384,7 +385,22 @@ def __init__(
384385
have a channel set. Default is 1.
385386
:param tz:
386387
Timezone for timestamps in the log file. Defaults to local timezone.
388+
:param timestamps_format:
389+
the format of timestamps in the header.
390+
Use ``"absolute"`` (default) so that readers can recover
391+
the original wall-clock timestamps by combining the
392+
per-message offset with the trigger-block start time.
393+
Use ``"relative"`` when only the elapsed time from the
394+
start of the recording matters and no absolute time
395+
recovery is needed.
396+
:raises ValueError: if *timestamps_format* is not ``"absolute"`` or
397+
``"relative"``
387398
"""
399+
if timestamps_format not in ("absolute", "relative"):
400+
raise ValueError(
401+
f"timestamps_format must be 'absolute' or 'relative', "
402+
f"got {timestamps_format!r}"
403+
)
388404
if kwargs.get("append", False):
389405
raise ValueError(
390406
f"{self.__class__.__name__} is currently not equipped to "
@@ -394,11 +410,12 @@ def __init__(
394410

395411
self._timezone = tz
396412
self.channel = channel
413+
self.timestamps_format = timestamps_format
397414

398415
# write start of file header
399416
start_time = self._format_header_datetime(datetime.now(tz=self._timezone))
400417
self.file.write(f"date {start_time}\n")
401-
self.file.write("base hex timestamps absolute\n")
418+
self.file.write(f"base hex timestamps {self.timestamps_format}\n")
402419
self.file.write("internal events logged\n")
403420

404421
# the last part is written with the timestamp of the first message

test/logformats_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,49 @@ def test_write(self):
687687

688688
self.assertEqual(expected_file.read_text(), actual_file.read_text())
689689

690+
def test_write_timestamps_format_default_is_absolute(self):
691+
"""ASCWriter should write 'timestamps absolute' in the header by default."""
692+
with can.ASCWriter(self.test_file_name) as writer:
693+
pass
694+
695+
content = Path(self.test_file_name).read_text()
696+
self.assertIn("timestamps absolute", content)
697+
698+
def test_write_timestamps_format_relative(self):
699+
"""ASCWriter should write 'timestamps relative' when requested."""
700+
with can.ASCWriter(self.test_file_name, timestamps_format="relative") as writer:
701+
pass
702+
703+
content = Path(self.test_file_name).read_text()
704+
self.assertIn("timestamps relative", content)
705+
self.assertNotIn("timestamps absolute", content)
706+
707+
def test_write_timestamps_format_invalid(self):
708+
"""ASCWriter should raise ValueError for an unsupported timestamps_format."""
709+
with self.assertRaises(ValueError):
710+
can.ASCWriter(self.test_file_name, timestamps_format="unix")
711+
712+
def test_write_relative_timestamp_roundtrip(self):
713+
"""Messages written with relative format round-trip with relative timestamps."""
714+
msgs = [
715+
can.Message(timestamp=100.0, arbitration_id=0x1, data=b"\x01"),
716+
can.Message(timestamp=100.5, arbitration_id=0x2, data=b"\x02"),
717+
]
718+
719+
with can.ASCWriter(
720+
self.test_file_name, timestamps_format="relative"
721+
) as writer:
722+
for m in msgs:
723+
writer.on_message_received(m)
724+
725+
with can.ASCReader(self.test_file_name, relative_timestamp=True) as reader:
726+
result = list(reader)
727+
728+
self.assertEqual(len(result), len(msgs))
729+
# With relative_timestamp=True timestamps are offsets from the first message
730+
self.assertAlmostEqual(result[0].timestamp, 0.0, places=5)
731+
self.assertAlmostEqual(result[1].timestamp, 0.5, places=5)
732+
690733
@parameterized.expand(
691734
[
692735
(

0 commit comments

Comments
 (0)