Skip to content

Commit 39a2c7f

Browse files
authored
add loop arg to player script (#1986)
1 parent f17c376 commit 39a2c7f

File tree

4 files changed

+117
-35
lines changed

4 files changed

+117
-35
lines changed

can/player.py

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import argparse
99
import errno
10+
import math
1011
import sys
1112
from datetime import datetime
1213
from typing import TYPE_CHECKING, cast
@@ -26,19 +27,41 @@
2627
from can import Message
2728

2829

30+
def _parse_loop(value: str) -> int | float:
31+
"""Parse the loop argument, allowing integer or 'i' for infinite."""
32+
if value == "i":
33+
return float("inf")
34+
try:
35+
return int(value)
36+
except ValueError as exc:
37+
err_msg = "Loop count must be an integer or 'i' for infinite."
38+
raise argparse.ArgumentTypeError(err_msg) from exc
39+
40+
41+
def _format_player_start_message(iteration: int, loop_count: int | float) -> str:
42+
"""
43+
Generate a status message indicating the start of a CAN log replay iteration.
44+
45+
:param iteration:
46+
The current loop iteration (zero-based).
47+
:param loop_count:
48+
Total number of replay loops, or infinity for endless replay.
49+
:return:
50+
A formatted string describing the replay start and loop information.
51+
"""
52+
if loop_count < 2:
53+
loop_info = ""
54+
else:
55+
loop_val = "∞" if math.isinf(loop_count) else str(loop_count)
56+
loop_info = f" [loop {iteration + 1}/{loop_val}]"
57+
return f"Can LogReader (Started on {datetime.now()}){loop_info}"
58+
59+
2960
def main() -> None:
3061
parser = argparse.ArgumentParser(description="Replay CAN traffic.")
3162

3263
player_group = parser.add_argument_group("Player arguments")
3364

34-
player_group.add_argument(
35-
"-f",
36-
"--file_name",
37-
dest="log_file",
38-
help="Path and base log filename, for supported types see can.LogReader.",
39-
default=None,
40-
)
41-
4265
player_group.add_argument(
4366
"-v",
4467
action="count",
@@ -73,9 +96,20 @@ def main() -> None:
7396
"--skip",
7497
type=float,
7598
default=60 * 60 * 24,
76-
help="<s> skip gaps greater than 's' seconds",
99+
help="Skip gaps greater than 's' seconds between messages. "
100+
"Default is 86400 (24 hours), meaning only very large gaps are skipped. "
101+
"Set to 0 to never skip any gaps (all delays are preserved). "
102+
"Set to a very small value (e.g., 1e-4) "
103+
"to skip all gaps and send messages as fast as possible.",
104+
)
105+
player_group.add_argument(
106+
"-l",
107+
"--loop",
108+
type=_parse_loop,
109+
metavar="NUM",
110+
default=1,
111+
help="Replay file NUM times. Use 'i' for infinite loop (default: 1)",
77112
)
78-
79113
player_group.add_argument(
80114
"infile",
81115
metavar="input-file",
@@ -103,25 +137,28 @@ def main() -> None:
103137
error_frames = results.error_frames
104138

105139
with create_bus_from_namespace(results) as bus:
106-
with LogReader(results.infile, **additional_config) as reader:
107-
in_sync = MessageSync(
108-
cast("Iterable[Message]", reader),
109-
timestamps=results.timestamps,
110-
gap=results.gap,
111-
skip=results.skip,
112-
)
113-
114-
print(f"Can LogReader (Started on {datetime.now()})")
115-
116-
try:
117-
for message in in_sync:
118-
if message.is_error_frame and not error_frames:
119-
continue
120-
if verbosity >= 3:
121-
print(message)
122-
bus.send(message)
123-
except KeyboardInterrupt:
124-
pass
140+
loop_count: int | float = results.loop
141+
iteration = 0
142+
try:
143+
while iteration < loop_count:
144+
with LogReader(results.infile, **additional_config) as reader:
145+
in_sync = MessageSync(
146+
cast("Iterable[Message]", reader),
147+
timestamps=results.timestamps,
148+
gap=results.gap,
149+
skip=results.skip,
150+
)
151+
print(_format_player_start_message(iteration, loop_count))
152+
153+
for message in in_sync:
154+
if message.is_error_frame and not error_frames:
155+
continue
156+
if verbosity >= 3:
157+
print(message)
158+
bus.send(message)
159+
iteration += 1
160+
except KeyboardInterrupt:
161+
pass
125162

126163

127164
if __name__ == "__main__":

doc/changelog.d/1815.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for replaying CAN log files multiple times or infinitely in the player script via the new --loop/-l argument.

doc/changelog.d/1815.removed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Removed the unused --file_name/-f argument from the player CLI.

test/test_player.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from unittest import mock
1212
from unittest.mock import Mock
1313

14+
from parameterized import parameterized
15+
1416
import can
1517
import can.player
1618

@@ -38,7 +40,7 @@ def assertSuccessfulCleanup(self):
3840
self.mock_virtual_bus.__exit__.assert_called_once()
3941

4042
def test_play_virtual(self):
41-
sys.argv = self.baseargs + [self.logfile]
43+
sys.argv = [*self.baseargs, self.logfile]
4244
can.player.main()
4345
msg1 = can.Message(
4446
timestamp=2.501,
@@ -65,8 +67,8 @@ def test_play_virtual(self):
6567
self.assertSuccessfulCleanup()
6668

6769
def test_play_virtual_verbose(self):
68-
sys.argv = self.baseargs + ["-v", self.logfile]
69-
with unittest.mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
70+
sys.argv = [*self.baseargs, "-v", self.logfile]
71+
with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
7072
can.player.main()
7173
self.assertIn("09 08 07 06 05 04 03 02", mock_stdout.getvalue())
7274
self.assertIn("05 0c 00 00 00 00 00 00", mock_stdout.getvalue())
@@ -76,7 +78,7 @@ def test_play_virtual_verbose(self):
7678
def test_play_virtual_exit(self):
7779
self.MockSleep.side_effect = [None, KeyboardInterrupt]
7880

79-
sys.argv = self.baseargs + [self.logfile]
81+
sys.argv = [*self.baseargs, self.logfile]
8082
can.player.main()
8183
assert self.mock_virtual_bus.send.call_count <= 2
8284
self.assertSuccessfulCleanup()
@@ -85,7 +87,7 @@ def test_play_skip_error_frame(self):
8587
logfile = os.path.join(
8688
os.path.dirname(__file__), "data", "logfile_errorframes.asc"
8789
)
88-
sys.argv = self.baseargs + ["-v", logfile]
90+
sys.argv = [*self.baseargs, "-v", logfile]
8991
can.player.main()
9092
self.assertEqual(self.mock_virtual_bus.send.call_count, 9)
9193
self.assertSuccessfulCleanup()
@@ -94,11 +96,52 @@ def test_play_error_frame(self):
9496
logfile = os.path.join(
9597
os.path.dirname(__file__), "data", "logfile_errorframes.asc"
9698
)
97-
sys.argv = self.baseargs + ["-v", "--error-frames", logfile]
99+
sys.argv = [*self.baseargs, "-v", "--error-frames", logfile]
98100
can.player.main()
99101
self.assertEqual(self.mock_virtual_bus.send.call_count, 12)
100102
self.assertSuccessfulCleanup()
101103

104+
@parameterized.expand([0, 1, 2, 3])
105+
def test_play_loop(self, loop_val):
106+
sys.argv = [*self.baseargs, "--loop", str(loop_val), self.logfile]
107+
can.player.main()
108+
msg1 = can.Message(
109+
timestamp=2.501,
110+
arbitration_id=0xC8,
111+
is_extended_id=False,
112+
is_fd=False,
113+
is_rx=False,
114+
channel=1,
115+
dlc=8,
116+
data=[0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2],
117+
)
118+
msg2 = can.Message(
119+
timestamp=17.876708,
120+
arbitration_id=0x6F9,
121+
is_extended_id=False,
122+
is_fd=False,
123+
is_rx=True,
124+
channel=0,
125+
dlc=8,
126+
data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0],
127+
)
128+
for i in range(loop_val):
129+
self.assertTrue(
130+
msg1.equals(self.mock_virtual_bus.send.mock_calls[2 * i + 0].args[0])
131+
)
132+
self.assertTrue(
133+
msg2.equals(self.mock_virtual_bus.send.mock_calls[2 * i + 1].args[0])
134+
)
135+
self.assertEqual(self.mock_virtual_bus.send.call_count, 2 * loop_val)
136+
self.assertSuccessfulCleanup()
137+
138+
def test_play_loop_infinite(self):
139+
self.mock_virtual_bus.send.side_effect = [None] * 99 + [KeyboardInterrupt]
140+
sys.argv = [*self.baseargs, "-l", "i", self.logfile]
141+
can.player.main()
142+
self.assertEqual(self.mock_virtual_bus.send.call_count, 100)
143+
self.assertSuccessfulCleanup()
144+
102145

103146
class TestPlayerCompressedFile(TestPlayerScriptModule):
104147
"""

0 commit comments

Comments
 (0)