Skip to content

Commit 3b18d88

Browse files
committed
Implement action recording and display info about recorded action
Signed-off-by: Barry Xu <[email protected]>
1 parent 3c6ef95 commit 3b18d88

File tree

53 files changed

+2910
-268
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2910
-268
lines changed

ros2bag/ros2bag/api/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,19 @@ def convert_service_to_service_event_topic(services):
321321
services_event_topics.append(service + '/_service_event')
322322

323323
return services_event_topics
324+
325+
326+
def convert_action_to_all_related_topics(actions):
327+
action_topics = []
328+
329+
if not actions:
330+
return action_topics
331+
332+
for action in actions:
333+
action_topics.append(action + '/_action/send_goal/_service_event')
334+
action_topics.append(action + '/_action/get_result/_service_event')
335+
action_topics.append(action + '/_action/cancel_goal/_service_event')
336+
action_topics.append(action + '/_action/status')
337+
action_topics.append(action + '/_action/feedback')
338+
339+
return action_topics

ros2bag/ros2bag/verb/info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ def add_arguments(self, parser, cli_name): # noqa: D102
2828
)
2929
parser.add_argument(
3030
'-v', '--verbose', action='store_true',
31-
help='Display request/response information for services'
31+
help='Display request/response information for services. Display request/response '
32+
'information for action internal services'
3233
)
3334
available_sorting_methods = Info().get_sorting_methods()
3435
parser.add_argument(

ros2bag/ros2bag/verb/record.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from rclpy.qos import InvalidQoSProfileException
2020
from ros2bag.api import add_writer_storage_plugin_extensions
21+
from ros2bag.api import convert_action_to_all_related_topics
2122
from ros2bag.api import convert_service_to_service_event_topic
2223
from ros2bag.api import convert_yaml_to_qos_profile
2324
from ros2bag.api import print_error
@@ -70,27 +71,34 @@ def add_recorder_arguments(parser: ArgumentParser) -> None:
7071
parser.add_argument(
7172
'--services', type=str, metavar='ServiceName', nargs='+',
7273
help='Space-delimited list of services to record.')
74+
parser.add_argument(
75+
'--actions', type=str, metavar='ActionName', nargs='+',
76+
help='Space-delimited list of actions to record.')
7377
parser.add_argument(
7478
'--topic-types', nargs='+', default=[], metavar='TopicType',
7579
help='Space-delimited list of topic types to record.')
7680
parser.add_argument(
7781
'-a', '--all', action='store_true',
78-
help='Record all topics and services (Exclude hidden topic).')
82+
help='Record all topics, services and actions (Exclude hidden topic).')
7983
parser.add_argument(
8084
'--all-topics', action='store_true',
8185
help='Record all topics (Exclude hidden topic).')
8286
parser.add_argument(
8387
'--all-services', action='store_true',
8488
help='Record all services via service event topics.')
89+
parser.add_argument(
90+
'--all-actions', action='store_true',
91+
help='Record all actions via internal topics and service event topics.')
8592
parser.add_argument(
8693
'-e', '--regex', default='',
8794
help='Record only topics and services containing provided regular expression. '
88-
'Note: --all, --all-topics or --all-services will override --regex.')
95+
'Note: --all, --all-topics, --all-services or --all-actions will override --regex.')
8996
parser.add_argument(
9097
'--exclude-regex', default='',
9198
help='Exclude topics and services containing provided regular expression. '
9299
'Works on top of '
93-
'--all, --all-topics, --all-services, --topics, --services or --regex.')
100+
'--all, --all-topics, --all-services, --all-actions, --topics, --services, --actions '
101+
'or --regex.')
94102
parser.add_argument(
95103
'--exclude-topic-types', type=str, default=[], metavar='ExcludeTopicTypes', nargs='+',
96104
help='Space-delimited list of topic types not being recorded. '
@@ -103,6 +111,10 @@ def add_recorder_arguments(parser: ArgumentParser) -> None:
103111
'--exclude-services', type=str, metavar='ServiceName', nargs='+',
104112
help='Space-delimited list of services not being recorded. '
105113
'Works on top of --all, --all-services, --services or --regex.')
114+
parser.add_argument(
115+
'--exclude-actions', type=str, metavar='ActionName', nargs='+',
116+
help='Space-delimited list of actions not being recorded. '
117+
'Works on top of --all, --all-actions, --actions or --regex.')
106118

107119
# Discovery behavior
108120
parser.add_argument(
@@ -217,10 +229,11 @@ def add_recorder_arguments(parser: ArgumentParser) -> None:
217229

218230

219231
def check_necessary_argument(args):
220-
# At least one options out of --all, --all-topics, --all-services, --services, --topics,
221-
# --topic-types or --regex must be used
222-
if not (args.all or args.all_topics or args.all_services or
232+
# At least one options out of --all, --all-topics, --all-services, --all-actions, --services,
233+
# --actions --topics, --topic-types or --regex must be used
234+
if not (args.all or args.all_topics or args.all_services or args.all_actions or
223235
(args.services and len(args.services) > 0) or
236+
(args.actions and len(args.actions) > 0) or
224237
(args.topics and len(args.topics) > 0) or
225238
(args.topic_types and len(args.topic_types) > 0) or args.regex):
226239
return False
@@ -239,9 +252,9 @@ def validate_parsed_arguments(args, uri) -> str:
239252

240253
if args.exclude_regex and not \
241254
(args.all or args.all_topics or args.topic_types or args.all_services or
242-
args.regex):
255+
args.all_actions or args.regex):
243256
return print_error('--exclude-regex argument requires either --all, '
244-
'--all-topics, --topic-types, --all-services or --regex')
257+
'--all-topics, --topic-types, --all-services, --all-actions or --regex')
245258

246259
if args.exclude_topics and not \
247260
(args.all or args.all_topics or args.topic_types or args.regex):
@@ -257,14 +270,22 @@ def validate_parsed_arguments(args, uri) -> str:
257270
return print_error('--exclude-services argument requires either --all, --all-services '
258271
'or --regex')
259272

273+
if args.exclude_actions and not (args.all or args.all_actions or args.regex):
274+
return print_error('--exclude-actions argument requires either --all, --all-actions '
275+
'or --regex')
276+
260277
if (args.all or args.all_services) and args.services:
261278
print(print_warn('--all or --all-services will override --services'))
262279

263280
if (args.all or args.all_topics) and args.topics:
264281
print(print_warn('--all or --all-topics will override --topics'))
265282

266-
if (args.all or args.all_topics or args.all_services) and args.regex:
267-
print(print_warn('--all, --all-topics or --all-services will override --regex'))
283+
if (args.all or args.all_actions) and args.actions:
284+
print(print_warn('--all or --all-actions will override --actions'))
285+
286+
if (args.all or args.all_topics or args.all_services or args.all_actions) and args.regex:
287+
print(print_warn('--all, --all-topics --all-services or --all-actions will override '
288+
'--regex'))
268289

269290
if os.path.isdir(uri):
270291
return print_error("Output folder '{}' already exists.".format(uri))
@@ -281,6 +302,8 @@ def validate_parsed_arguments(args, uri) -> str:
281302
if args.compression_queue_size < 0:
282303
return print_error('Compression queue size must be at least 0.')
283304

305+
return ''
306+
284307

285308
class RecordVerb(VerbExtension):
286309
"""Record ROS data to a bag."""
@@ -330,11 +353,15 @@ def main(self, *, args): # noqa: D102
330353
record_options = RecordOptions()
331354
record_options.all_topics = args.all_topics or args.all
332355
record_options.all_services = args.all_services or args.all
356+
record_options.all_actions = args.all_actions or args.all
333357
record_options.is_discovery_disabled = args.no_discovery
334358
record_options.topics = args.topics
335359
record_options.topic_types = args.topic_types
336360
# Convert service name to service event topic name
337361
record_options.services = convert_service_to_service_event_topic(args.services)
362+
# Covert action name to internal topic name and internal service event topic name
363+
record_options.actions = convert_action_to_all_related_topics(args.actions)
364+
338365
record_options.exclude_topic_types = args.exclude_topic_types
339366
record_options.rmw_serialization_format = args.serialization_format
340367
record_options.topic_polling_interval = datetime.timedelta(
@@ -344,6 +371,7 @@ def main(self, *, args): # noqa: D102
344371
record_options.exclude_topics = args.exclude_topics if args.exclude_topics else []
345372
record_options.exclude_service_events = \
346373
convert_service_to_service_event_topic(args.exclude_services)
374+
record_options.exclude_actions = convert_action_to_all_related_topics(args.exclude_actions)
347375
record_options.node_prefix = NODE_NAME_PREFIX
348376
record_options.compression_mode = args.compression_mode
349377
record_options.compression_format = args.compression_format

ros2bag/test/test_recorder_args_parser.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ def test_recorder_services_list_argument(test_arguments_parser):
6363
assert output_path.as_posix() == args.output
6464

6565

66+
def test_recorder_actions_list_argument(test_arguments_parser):
67+
"""Test recorder --actions list argument parser."""
68+
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
69+
args = test_arguments_parser.parse_args(
70+
['--actions', 'action1', 'action2', '--output', output_path.as_posix()]
71+
)
72+
assert ['action1', 'action2'] == args.actions
73+
assert output_path.as_posix() == args.output
74+
75+
6676
def test_recorder_services_and_positional_topics_list_arguments(test_arguments_parser):
6777
"""Test recorder --services list and positional topics list arguments parser."""
6878
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
@@ -74,6 +84,17 @@ def test_recorder_services_and_positional_topics_list_arguments(test_arguments_p
7484
assert output_path.as_posix() == args.output
7585

7686

87+
def test_recorder_actions_and_positional_topics_list_arguments(test_arguments_parser):
88+
"""Test recorder --actions list and positional topics list arguments parser."""
89+
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
90+
args = test_arguments_parser.parse_args(
91+
['--output', output_path.as_posix(),
92+
'--actions', 'action1', 'action2', '--', 'topic1', 'topic2'])
93+
assert ['action1', 'action2'] == args.actions
94+
assert ['topic1', 'topic2'] == args.topics_positional
95+
assert output_path.as_posix() == args.output
96+
97+
7798
def test_recorder_services_and_optional_topics_list_arguments(test_arguments_parser):
7899
"""Test recorder --services list and optional --topics list arguments parser."""
79100
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
@@ -85,6 +106,41 @@ def test_recorder_services_and_optional_topics_list_arguments(test_arguments_par
85106
assert output_path.as_posix() == args.output
86107

87108

109+
def test_recorder_actions_and_optional_topics_list_arguments(test_arguments_parser):
110+
"""Test recorder --actions list and optional --topics list arguments parser."""
111+
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
112+
args = test_arguments_parser.parse_args(
113+
['--output', output_path.as_posix(),
114+
'--actions', 'action1', 'action2', '--topics', 'topic1', 'topic2'])
115+
assert ['action1', 'action2'] == args.actions
116+
assert ['topic1', 'topic2'] == args.topics
117+
assert output_path.as_posix() == args.output
118+
119+
120+
def test_recorder_services_and_actions_list_arguments(test_arguments_parser):
121+
"""Test recorder --services list and --actions list arguments parser."""
122+
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
123+
args = test_arguments_parser.parse_args(
124+
['--output', output_path.as_posix(),
125+
'--services', 'service1', 'service2', '--actions', 'action1', 'action2'])
126+
assert ['service1', 'service2'] == args.services
127+
assert ['action1', 'action2'] == args.actions
128+
assert output_path.as_posix() == args.output
129+
130+
131+
def test_recorder_services_actions_and_topics_list_arguments(test_arguments_parser):
132+
"""Test recorder --services list, --actions list and --topics list arguments parser."""
133+
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
134+
args = test_arguments_parser.parse_args(
135+
['--output', output_path.as_posix(),
136+
'--services', 'service1', 'service2', '--actions', 'action1', 'action2',
137+
'--topics', 'topic1', 'topic2'])
138+
assert ['service1', 'service2'] == args.services
139+
assert ['action1', 'action2'] == args.actions
140+
assert ['topic1', 'topic2'] == args.topics
141+
assert output_path.as_posix() == args.output
142+
143+
88144
def test_recorder_topic_types_list_argument(test_arguments_parser):
89145
"""Test recorder --topic-types list argument parser."""
90146
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
@@ -124,6 +180,16 @@ def test_recorder_exclude_services_list_argument(test_arguments_parser):
124180
assert output_path.as_posix() == args.output
125181

126182

183+
def test_recorder_exclude_actions_list_argument(test_arguments_parser):
184+
"""Test recorder --exclude-actions list argument parser."""
185+
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
186+
args = test_arguments_parser.parse_args(
187+
['--exclude-actions', 'action1', 'action2', '--output', output_path.as_posix()]
188+
)
189+
assert ['action1', 'action2'] == args.exclude_actions
190+
assert output_path.as_posix() == args.output
191+
192+
127193
def test_recorder_custom_data_list_argument(test_arguments_parser):
128194
"""Test recorder --custom-data list argument parser."""
129195
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
@@ -138,21 +204,22 @@ def test_recorder_validate_exclude_regex_needs_inclusive_args(test_arguments_par
138204
"""Test that --exclude-regex needs inclusive arguments."""
139205
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
140206
args = test_arguments_parser.parse_args(
141-
['--exclude-regex', '%s-%s', '--services', 'service1', '--topics', 'topic1',
142-
'--output', output_path.as_posix()]
207+
['--exclude-regex', '%s-%s', '--services', 'service1', '--topics', 'topic1', '--actions',
208+
'action1', '--output', output_path.as_posix()]
143209
)
144210
assert '%s-%s' == args.exclude_regex
145211
assert args.all is False
146212
assert args.all_topics is False
147213
assert [] == args.topic_types
148214
assert args.all_services is False
215+
assert args.all_actions is False
149216
assert '' == args.regex
150217

151218
uri = args.output or datetime.datetime.now().strftime('rosbag2_%Y_%m_%d-%H_%M_%S')
152219
error_str = validate_parsed_arguments(args, uri)
153220
assert error_str is not None
154221
expected_output = '--exclude-regex argument requires either --all, ' \
155-
'--all-topics, --topic-types, --all-services or --regex'
222+
'--all-topics, --topic-types, --all-services, --all-actions or --regex'
156223
matches = expected_output in error_str
157224
assert matches, ERROR_STRING_MSG.format(expected_output, error_str)
158225

@@ -227,3 +294,26 @@ def test_recorder_validate_exclude_services_needs_inclusive_args(test_arguments_
227294
'or --regex'
228295
matches = expected_output in error_str
229296
assert matches, ERROR_STRING_MSG.format(expected_output, error_str)
297+
298+
299+
def test_recorder_validate_exclude_actions_needs_inclusive_args(test_arguments_parser):
300+
"""Test that --exclude-actions needs at least --all, --all-actions or --regex arguments."""
301+
output_path = RESOURCES_PATH / 'ros2bag_tmp_file'
302+
args = test_arguments_parser.parse_args(
303+
['--exclude-actions', 'action1', '--actions', 'action1', '--all-topics',
304+
'--output', output_path.as_posix()]
305+
)
306+
assert ['action1'] == args.exclude_actions
307+
assert args.all is False
308+
assert args.all_topics is True
309+
assert [] == args.topic_types
310+
assert args.all_services is False
311+
assert '' == args.regex
312+
assert '' == args.exclude_regex
313+
314+
uri = args.output or datetime.datetime.now().strftime('rosbag2_%Y_%m_%d-%H_%M_%S')
315+
error_str = validate_parsed_arguments(args, uri)
316+
assert error_str is not None
317+
expected_output = '--exclude-actions argument requires either --all, --all-actions or --regex'
318+
matches = expected_output in error_str
319+
assert matches, ERROR_STRING_MSG.format(expected_output, error_str)

rosbag2_cpp/CMakeLists.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ add_library(${PROJECT_NAME} SHARED
7373
src/rosbag2_cpp/writer.cpp
7474
src/rosbag2_cpp/writers/sequential_writer.cpp
7575
src/rosbag2_cpp/reindexer.cpp
76-
src/rosbag2_cpp/service_utils.cpp)
76+
src/rosbag2_cpp/service_utils.cpp
77+
src/rosbag2_cpp/action_utils.cpp)
7778

7879
target_link_libraries(${PROJECT_NAME} PUBLIC
7980
pluginlib::pluginlib
@@ -303,6 +304,16 @@ if(BUILD_TESTING)
303304
${test_msgs_TARGETS}
304305
)
305306
endif()
307+
308+
ament_add_gmock(test_action_utils
309+
test/rosbag2_cpp/test_action_utils.cpp)
310+
if(TARGET test_action_utils)
311+
target_link_libraries(test_action_utils
312+
${PROJECT_NAME}
313+
rosbag2_test_common::rosbag2_test_common
314+
${test_msgs_TARGETS}
315+
)
316+
endif()
306317
endif()
307318

308319
ament_package()

0 commit comments

Comments
 (0)