Skip to content

Commit d31044d

Browse files
Support both parameter file configurations for composable nodes (#259)
* Support both param file configurations in to_parameters_list.py Signed-off-by: Rebecca Butler <[email protected]> * Support another file config and add tests Signed-off-by: Rebecca Butler <[email protected]> * Update to work for multiple entries in yaml Signed-off-by: Rebecca Butler <[email protected]> * Fix deleted line Signed-off-by: Rebecca Butler <[email protected]> * Remove duplicated line Signed-off-by: Rebecca Butler <[email protected]> * Address reviewer comments Signed-off-by: Jacob Perron <[email protected]> * Emit warning iff no node name and ros__parameters foramt is used Signed-off-by: Jacob Perron <[email protected]> * Refactor after review - Rename private function for dealing with ros__parameters entries - Keep recursive parameters internal to function - Skip evaluating parameters if dictionary is empty - Use isinstance - Strip trailing and leading '/' from node namespace Signed-off-by: Jacob Perron <[email protected]> * Remove unnecessary line Signed-off-by: Jacob Perron <[email protected]> * Reset keys list in case of dictionary value We actually do this for the case of multiple entries like this: /ns_1: /node_1: ros__parameters: param_1: 1 param_2: 2 /**: ros__parameters: param_2: 22 param_3: 33 Signed-off-by: Jacob Perron <[email protected]> * Use separate test file for test_load_composable_nodes Rather than reusing the test file that is used in test_node. Signed-off-by: Jacob Perron <[email protected]> Co-authored-by: Jacob Perron <[email protected]>
1 parent 4bfd998 commit d31044d

File tree

9 files changed

+271
-6
lines changed

9 files changed

+271
-6
lines changed

launch_ros/launch_ros/actions/load_composable_nodes.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def get_composable_node_load_request(
243243
composable_node_description: ComposableNode,
244244
context: LaunchContext
245245
):
246-
"""Get the request that will be send to the composable node container."""
246+
"""Get the request that will be sent to the composable node container."""
247247
request = composition_interfaces.srv.LoadNode.Request()
248248
request.package_name = perform_substitutions(
249249
context, composable_node_description.package
@@ -286,15 +286,17 @@ def get_composable_node_load_request(
286286
if parameters:
287287
request.parameters = [
288288
param.to_parameter_msg() for param in to_parameters_list(
289-
context, evaluate_parameters(
289+
context, request.node_name, expanded_ns,
290+
evaluate_parameters(
290291
context, parameters
291292
)
292293
)
293294
]
294295
if composable_node_description.extra_arguments is not None:
295296
request.extra_arguments = [
296297
param.to_parameter_msg() for param in to_parameters_list(
297-
context, evaluate_parameters(
298+
context, request.node_name, expanded_ns,
299+
evaluate_parameters(
298300
context, composable_node_description.extra_arguments
299301
)
300302
)

launch_ros/launch_ros/utilities/to_parameters_list.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import pathlib
1818
from typing import List
19+
import warnings
1920

2021
from launch.launch_context import LaunchContext
2122
import rclpy.parameter
@@ -28,26 +29,81 @@
2829
from ..parameters_type import EvaluatedParameters
2930

3031

32+
def __normalize_parameters_dict(dictionary):
33+
"""
34+
Combine namespaces and their node names into one key and remove the "ros__parameters" key.
35+
36+
Return an empty dict if the "ros__parameters" key is not found.
37+
"""
38+
# Recursive function
39+
def normalize_parameters_dict(dictionary, keys, result_dict):
40+
for key, value in dictionary.items():
41+
# Base case: found 'ros__parameters' key
42+
# Return
43+
if key == 'ros__parameters':
44+
node_name = '/'.join(keys)
45+
result_dict[node_name] = value
46+
return result_dict
47+
48+
if isinstance(value, dict):
49+
keys.append(key.lstrip('/'))
50+
result_dict = normalize_parameters_dict(value, keys, result_dict)
51+
# Reset keys in case there are multiple ros__parameter entries
52+
keys = []
53+
54+
return result_dict
55+
56+
return normalize_parameters_dict(dictionary, [], {})
57+
58+
3159
def to_parameters_list(
3260
context: LaunchContext,
61+
node_name: str,
62+
namespace: str,
3363
evaluated_parameters: EvaluatedParameters
3464
) -> List[rclpy.parameter.Parameter]:
3565
"""
3666
Transform evaluated parameters into a list of rclpy.parameter.Parameter objects.
3767
3868
:param context: to carry out substitutions during normalization of parameter files.
3969
See `normalize_parameters()` documentation for further reference.
70+
:param node_name: node name
71+
:param namespace: node namespace
4072
:param parameters: parameters as either paths to parameter files or name/value pairs.
4173
See `evaluate_parameters()` documentation for further reference.
4274
:returns: a list of parameters
4375
"""
4476
parameters = [] # type: List[rclpy.parameter.Parameter]
77+
node_name = node_name.lstrip('/')
78+
if namespace:
79+
namespace = namespace.strip('/')
80+
node_name = f'{namespace}/{node_name}'
81+
82+
params_set = {}
83+
warned_once = False
4584
for params_set_or_path in evaluated_parameters:
4685
if isinstance(params_set_or_path, pathlib.Path):
4786
with open(str(params_set_or_path), 'r') as f:
48-
params_set = evaluate_parameter_dict(
49-
context, normalize_parameter_dict(yaml.safe_load(f))
50-
)
87+
param_dict = yaml.safe_load(f)
88+
normalized_param_dict = __normalize_parameters_dict(param_dict)
89+
90+
if normalized_param_dict:
91+
param_dict.clear()
92+
if '**' in normalized_param_dict:
93+
param_dict = normalized_param_dict['**']
94+
if node_name in normalized_param_dict:
95+
param_dict.update(normalized_param_dict[node_name])
96+
if not warned_once and not node_name:
97+
warnings.warn(
98+
'node name not provided to launch; parameter files will not apply',
99+
UserWarning
100+
)
101+
warned_once = True
102+
103+
if param_dict:
104+
params_set = evaluate_parameter_dict(
105+
context, normalize_parameter_dict(param_dict)
106+
)
51107
else:
52108
params_set = params_set_or_path
53109
if not isinstance(params_set, dict):
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/ns_1:
2+
/node_1:
3+
ros__parameters:
4+
param_1: 1
5+
param_2: 2
6+
7+
/**:
8+
ros__parameters:
9+
param_2: 22
10+
param_3: 33
11+
12+
ns_2/node_2:
13+
ros__parameters:
14+
param_3: 3
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ns/test_node_name:
2+
ros__parameters:
3+
param: 1
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/my_ns:
2+
my_node:
3+
ros__parameters:
4+
some_int: 42
5+
a_string: "Hello world"
6+
no_string: ""
7+
some_list:
8+
sub_list:
9+
some_integers: [1, 2, 3, 4]
10+
some_doubles : [3.14, 2.718]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
some_int: 42
2+
a_string: "Hello world"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/test_node_name:
2+
ros__parameters:
3+
param: 2
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/**:
2+
ros__parameters:
3+
param: "wildcard"

test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Tests for the LoadComposableNodes Action."""
1616

17+
import pathlib
1718
import threading
1819

1920
from composition_interfaces.srv import LoadNode
@@ -214,6 +215,177 @@ def test_load_node_with_params(mock_component_container):
214215
assert len(request.extra_arguments) == 0
215216

216217

218+
def test_load_node_with_param_file(mock_component_container):
219+
"""Test loading a node with with parameters specified in yaml files."""
220+
parameters_file_dir = pathlib.Path(__file__).resolve().parent
221+
222+
# Case 1: no node name in yaml file
223+
context = _assert_launch_no_errors([
224+
_load_composable_node(
225+
package='foo_package',
226+
plugin='bar_plugin',
227+
name='test_node_name',
228+
namespace='test_node_namespace',
229+
parameters=[
230+
parameters_file_dir / 'example_parameters_no_name.yaml'
231+
],
232+
)
233+
])
234+
request = mock_component_container.requests[0]
235+
assert get_node_name_count(context, '/test_node_namespace/test_node_name') == 1
236+
assert request.node_name == 'test_node_name'
237+
assert request.node_namespace == '/test_node_namespace'
238+
assert len(request.parameters) == 2
239+
assert request.parameters[0].name == 'some_int'
240+
assert request.parameters[0].value.integer_value == 42
241+
assert request.parameters[1].name == 'a_string'
242+
assert request.parameters[1].value.string_value == 'Hello world'
243+
244+
# Case 2: node name with namespace
245+
context = _assert_launch_no_errors([
246+
_load_composable_node(
247+
package='foo_package',
248+
plugin='bar_plugin',
249+
name='test_node_name',
250+
namespace='ns',
251+
parameters=[
252+
parameters_file_dir / 'example_parameters_namespace.yaml'
253+
],
254+
)
255+
])
256+
request = mock_component_container.requests[1]
257+
assert get_node_name_count(context, '/ns/test_node_name') == 1
258+
assert request.node_name == 'test_node_name'
259+
assert request.node_namespace == '/ns'
260+
assert len(request.parameters) == 1
261+
assert request.parameters[0].name == 'param'
262+
assert request.parameters[0].value.integer_value == 1
263+
264+
# Case 3: nested node name with namespace
265+
context = _assert_launch_no_errors([
266+
_load_composable_node(
267+
package='foo_package',
268+
plugin='bar_plugin',
269+
name='my_node',
270+
namespace='my_ns',
271+
parameters=[
272+
parameters_file_dir / 'example_parameters_nested_namespace.yaml'
273+
],
274+
)
275+
])
276+
request = mock_component_container.requests[2]
277+
assert get_node_name_count(context, '/my_ns/my_node') == 1
278+
assert request.node_name == 'my_node'
279+
assert request.node_namespace == '/my_ns'
280+
assert len(request.parameters) == 5
281+
assert request.parameters[0].name == 'some_int'
282+
assert request.parameters[0].value.integer_value == 42
283+
assert request.parameters[1].name == 'a_string'
284+
assert request.parameters[1].value.string_value == 'Hello world'
285+
assert request.parameters[2].value.string_value == ''
286+
287+
# Case 4: node name without namespace
288+
context = _assert_launch_no_errors([
289+
_load_composable_node(
290+
package='foo_package',
291+
plugin='bar_plugin',
292+
name='test_node_name',
293+
namespace='',
294+
parameters=[
295+
parameters_file_dir / 'example_parameters_no_namespace.yaml'
296+
],
297+
)
298+
])
299+
request = mock_component_container.requests[3]
300+
assert get_node_name_count(context, '//test_node_name') == 1
301+
assert request.node_name == 'test_node_name'
302+
assert request.node_namespace == '/'
303+
assert len(request.parameters) == 1
304+
assert request.parameters[0].name == 'param'
305+
assert request.parameters[0].value.integer_value == 2
306+
307+
# Case 5: wildcard
308+
context = _assert_launch_no_errors([
309+
_load_composable_node(
310+
package='foo_package',
311+
plugin='bar_plugin',
312+
name='my_node',
313+
namespace='wildcard_ns',
314+
parameters=[
315+
parameters_file_dir / 'example_parameters_wildcard.yaml'
316+
],
317+
)
318+
])
319+
request = mock_component_container.requests[4]
320+
assert get_node_name_count(context, '/wildcard_ns/my_node') == 1
321+
assert request.node_name == 'my_node'
322+
assert request.node_namespace == '/wildcard_ns'
323+
assert len(request.parameters) == 1
324+
assert request.parameters[0].name == 'param'
325+
assert request.parameters[0].value.string_value == 'wildcard'
326+
327+
# Case 6: multiple entries (params with node name should take precedence over wildcard params)
328+
context = _assert_launch_no_errors([
329+
_load_composable_node(
330+
package='foo_package',
331+
plugin='bar_plugin',
332+
name='node_1',
333+
namespace='ns_1',
334+
parameters=[
335+
parameters_file_dir / 'example_parameters_multiple_entries.yaml'
336+
],
337+
),
338+
_load_composable_node(
339+
package='foo_package',
340+
plugin='bar_plugin',
341+
name='node_2',
342+
namespace='ns_2',
343+
parameters=[
344+
parameters_file_dir / 'example_parameters_multiple_entries.yaml'
345+
],
346+
)
347+
])
348+
request = mock_component_container.requests[5]
349+
assert get_node_name_count(context, '/ns_1/node_1') == 1
350+
assert request.node_name == 'node_1'
351+
assert request.node_namespace == '/ns_1'
352+
assert len(request.parameters) == 3
353+
assert request.parameters[0].name == 'param_2'
354+
assert request.parameters[0].value.integer_value == 2
355+
assert request.parameters[1].name == 'param_3'
356+
assert request.parameters[1].value.integer_value == 33
357+
assert request.parameters[2].name == 'param_1'
358+
assert request.parameters[2].value.integer_value == 1
359+
360+
request = mock_component_container.requests[6]
361+
assert get_node_name_count(context, '/ns_2/node_2') == 1
362+
assert request.node_name == 'node_2'
363+
assert request.node_namespace == '/ns_2'
364+
assert len(request.parameters) == 2
365+
assert request.parameters[0].name == 'param_2'
366+
assert request.parameters[0].value.integer_value == 22
367+
assert request.parameters[1].name == 'param_3'
368+
assert request.parameters[1].value.integer_value == 3
369+
370+
# Case 7: node name not found
371+
context = _assert_launch_no_errors([
372+
_load_composable_node(
373+
package='foo_package',
374+
plugin='bar_plugin',
375+
name='wrong_node_name',
376+
namespace='ns',
377+
parameters=[
378+
parameters_file_dir / 'example_parameters_no_namespace.yaml'
379+
],
380+
)
381+
])
382+
request = mock_component_container.requests[7]
383+
assert get_node_name_count(context, '/ns/wrong_node_name') == 1
384+
assert request.node_name == 'wrong_node_name'
385+
assert request.node_namespace == '/ns'
386+
assert len(request.parameters) == 0
387+
388+
217389
def test_load_node_with_global_remaps_in_group(mock_component_container):
218390
"""Test loading a node with global remaps scoped to a group."""
219391
context = _assert_launch_no_errors([

0 commit comments

Comments
 (0)