Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ The bag argument can be a directory containing `metadata.yaml` and one or more s
to a single storage file such as `.mcap` or `.db3`.
The Player will automatically detect which storage implementation to use for playing.
A progress bar to track the playback progress will be displayed in the terminal by default.
By default, the player process exits when playback is stopped or when it reaches the end of the
bag.

To play back multiple bags:

Expand Down Expand Up @@ -340,6 +342,11 @@ Options:
The reference to use for bag message chronological ordering.
Choices: reception timestamp (`received`), publication timestamp (`sent`).
Default: reception timestamp.
* `--persistent`:
Keep the player process alive after playback stops or reaches the end of the bag.
In persistent mode, playback can be started again without restarting `ros2 bag play`, including
future scheduled starts by setting `start_time` in the `~/play` service request.
Without this option, the player exits after playback stops or completes.
* `--progress-bar-update-rate [Hz]`:
Print a progress bar for the playback with a specified maximum update rate in times per second
(Hz). Negative values mark an update for every published message, while a zero value disables
Expand All @@ -350,6 +357,16 @@ Options:

For more options, run with `--help`.

To keep the player available for repeated service-driven playback runs, start it in persistent
mode:

```bash
$ ros2 bag play --persistent <bag>
```

With `--persistent`, the process stays alive after playback stops and only exits when explicitly
terminated, for example with `Ctrl+C`.

#### Playback action messages as action client

If you want Rosbag2 to replay recorded action messages in the role of an action client, you need to specify the --send-actions-as-client parameter.
Expand Down Expand Up @@ -395,7 +412,10 @@ The Rosbag2 player provides the following services for remote control, which can
* `~/set_rate [rosbag2_interfaces/srv/SetRate]`
* Sets the rate of playback, for example 2.0 will play messages twice as fast.
* `~/stop [rosbag2_interfaces/srv/Stop]`
* Stop the player, putting the play head in "undefined position" outside the bag. Must call `play` before other operations can be done.
* Stop the player, putting the play head in "undefined position" outside the bag. Must call `play`
before other operations can be done.
* Without `--persistent`, stopping playback causes the player process to exit. With
`--persistent`, the process stays alive and can be controlled again through services.
* `~/toggle_paused [rosbag2_interfaces/srv/TogglePaused]`
* Pause if playing, resume if paused.

Expand Down
18 changes: 14 additions & 4 deletions ros2bag/ros2bag/verb/play.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from argparse import FileType
import signal
import threading
import time

from rclpy.qos import InvalidQoSProfileException
from ros2bag.api import add_standard_multi_reader_args
Expand Down Expand Up @@ -207,6 +208,12 @@ def add_arguments(self, parser, cli_name): # noqa: D102
choices=['debug', 'info', 'warn', 'error', 'fatal'],
help='Logging level.')
progress_bar_group = parser.add_argument_group('Progress bar', 'Settings for progress bar')
parser.add_argument(
'--persistent',
action='store_true', default=False,
help='Keep the player process running after playback stops, allowing future '
'play/resume via service calls API.'
)
progress_bar_group.add_argument(
'--progress-bar-update-rate', type=int, metavar='Hz', default=3,
help='Print a progress bar for the playback with a specified maximum update rate in '
Expand Down Expand Up @@ -351,10 +358,13 @@ def main(self, *, args): # noqa: D102
player.play()
# Wait for playback to finish with periodic checks for termination
while not termination_requested.is_set():
# Use a short timeout to periodically check the termination flag
if player.wait_for_playback_to_finish_exclusively(0.1):
break # Playback finished naturally

if args.persistent:
# In persistent mode, just wait until termination is requested
time.sleep(0.1)
else:
# Use a short timeout to periodically check the termination flag
if player.wait_for_playback_to_finish_exclusively(0.1):
break # Playback finished naturally
# If termination was requested, the player stop will be called in the 'finally' block
except KeyboardInterrupt:
pass
Expand Down
60 changes: 60 additions & 0 deletions rosbag2_tests/test/rosbag2_tests/test_rosbag2_play_end_to_end.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

#include "rosbag2_interfaces/srv/resume.hpp"
#include "rosbag2_interfaces/srv/stop.hpp"
#include "rosbag2_interfaces/srv/play.hpp"
#include "rosbag2_test_common/process_execution_helpers.hpp"
#include "rosbag2_test_common/subscription_manager.hpp"
#include "rosbag2_test_common/tested_storage_ids.hpp"
Expand All @@ -42,6 +43,7 @@ class PlayEndToEndTestFixture : public Test, public WithParamInterface<std::stri
public:
using Resume = rosbag2_interfaces::srv::Resume;
using Stop = rosbag2_interfaces::srv::Stop;
using Play = rosbag2_interfaces::srv::Play;

PlayEndToEndTestFixture()
: sub_qos_(rclcpp::QoS{10}
Expand All @@ -54,6 +56,7 @@ class PlayEndToEndTestFixture : public Test, public WithParamInterface<std::stri
client_node_ = std::make_shared<rclcpp::Node>("test_player_client");
cli_resume_ = client_node_->create_client<Resume>("/rosbag2_player/resume");
cli_stop_ = client_node_->create_client<Stop>("/rosbag2_player/stop");
cli_play_ = client_node_->create_client<Play>("/rosbag2_player/play");
exec_ = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
exec_->add_node(client_node_);
spin_thread_ = std::thread(
Expand Down Expand Up @@ -113,6 +116,7 @@ class PlayEndToEndTestFixture : public Test, public WithParamInterface<std::stri
rclcpp::Node::SharedPtr client_node_;
rclcpp::Client<Resume>::SharedPtr cli_resume_;
rclcpp::Client<Stop>::SharedPtr cli_stop_;
rclcpp::Client<Play>::SharedPtr cli_play_;
std::thread spin_thread_;
std::shared_ptr<rclcpp::executors::SingleThreadedExecutor> exec_;
const std::chrono::seconds service_call_timeout_ {10};
Expand Down Expand Up @@ -357,6 +361,62 @@ TEST_P(PlayEndToEndTestFixture, play_end_to_end_exits_gracefully_on_sigterm) {
}
#endif // #ifndef _WIN32

TEST_P(PlayEndToEndTestFixture, play_persistent_allows_restart_via_play_service)
{
const std::string topic_name = "/test_topic";
sub_->add_subscription<test_msgs::msg::BasicTypes>(topic_name, 3, sub_qos_);

// Start ros2 bag play in pause + persistent mode
auto process_id =
start_execution("ros2 bag play --persistent -p " + bags_path_ +
"/cdr_test --topics " + topic_name);
auto cleanup_process_handle = rcpputils::make_scope_exit(
[process_id]() {
stop_execution(process_id);
});

EXPECT_TRUE(sub_->spin_and_wait_for_matched({topic_name}));

ASSERT_TRUE(cli_resume_->wait_for_service(service_call_timeout_));
successful_service_request<Resume>(cli_resume_);

sub_->spin_subscriptions_sync();
auto primitive_msgs = sub_->get_received_messages<test_msgs::msg::BasicTypes>(topic_name);
ASSERT_THAT(primitive_msgs, SizeIs(Ge(3u)));

// Stop playback via stop service
ASSERT_TRUE(cli_stop_->wait_for_service(service_call_timeout_));
successful_service_request<Stop>(cli_stop_);

sub_ = std::make_unique<SubscriptionManager>();
sub_->add_subscription<test_msgs::msg::BasicTypes>(topic_name, 3, sub_qos_);
EXPECT_TRUE(sub_->spin_and_wait_for_matched({topic_name}));

ASSERT_TRUE(cli_play_->wait_for_service(service_call_timeout_));
auto play_request = std::make_shared<Play::Request>();

play_request->start_time = rclcpp::Time(0, 0);
play_request->start_offset = rclcpp::Time(0, 0);
play_request->playback_duration = rclcpp::Duration(-1, 0);
play_request->playback_until_timestamp = rclcpp::Time(-1);

auto play_response =
successful_service_request<Play>(cli_play_, play_request);

ASSERT_TRUE(play_response);
EXPECT_EQ(play_response->return_code,
rosbag2_interfaces::srv::Play::Response::RETURN_CODE_SUCCESS);
EXPECT_TRUE(play_response->error_string.empty());

sub_->spin_subscriptions_sync();
primitive_msgs = sub_->get_received_messages<test_msgs::msg::BasicTypes>(topic_name);
ASSERT_THAT(primitive_msgs, SizeIs(Ge(3u)));

// Send SIGINT to child process and check exit code
stop_execution(process_id, SIGINT);
cleanup_process_handle.cancel();
}

INSTANTIATE_TEST_SUITE_P(
TestPlayEndToEnd,
PlayEndToEndTestFixture,
Expand Down
Loading