Skip to content

Commit bd54a04

Browse files
authored
Add a SetParameter action that sets a parameter to all nodes in the same scope (#158)
Signed-off-by: Ivan Santiago Paunovic <[email protected]>
1 parent 5098bfd commit bd54a04

File tree

8 files changed

+317
-55
lines changed

8 files changed

+317
-55
lines changed

launch_ros/launch_ros/actions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
from .load_composable_nodes import LoadComposableNodes
2020
from .node import Node
2121
from .push_ros_namespace import PushRosNamespace
22+
from .set_parameter import SetParameter
2223

2324
__all__ = [
2425
'ComposableNodeContainer',
2526
'LifecycleNode',
2627
'LoadComposableNodes',
2728
'Node',
2829
'PushRosNamespace',
30+
'SetParameter',
2931
]

launch_ros/launch_ros/actions/load_composable_nodes.py

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from ..utilities import evaluate_parameters
4040
from ..utilities import get_node_name_count
4141
from ..utilities import to_parameters_list
42+
from ..utilities.normalize_parameters import normalize_parameter_dict
4243

4344

4445
class LoadComposableNodes(Action):
@@ -96,44 +97,7 @@ def _load_node(
9697
)
9798
)
9899
return
99-
request = composition_interfaces.srv.LoadNode.Request()
100-
request.package_name = perform_substitutions(
101-
context, composable_node_description.package
102-
)
103-
request.plugin_name = perform_substitutions(
104-
context, composable_node_description.node_plugin
105-
)
106-
if composable_node_description.node_name is not None:
107-
request.node_name = perform_substitutions(
108-
context, composable_node_description.node_name
109-
)
110-
if composable_node_description.node_namespace is not None:
111-
request.node_namespace = perform_substitutions(
112-
context, composable_node_description.node_namespace
113-
)
114-
# request.log_level = perform_substitutions(context, node_description.log_level)
115-
if composable_node_description.remappings is not None:
116-
for from_, to in composable_node_description.remappings:
117-
request.remap_rules.append('{}:={}'.format(
118-
perform_substitutions(context, list(from_)),
119-
perform_substitutions(context, list(to)),
120-
))
121-
if composable_node_description.parameters is not None:
122-
request.parameters = [
123-
param.to_parameter_msg() for param in to_parameters_list(
124-
context, evaluate_parameters(
125-
context, composable_node_description.parameters
126-
)
127-
)
128-
]
129-
if composable_node_description.extra_arguments is not None:
130-
request.extra_arguments = [
131-
param.to_parameter_msg() for param in to_parameters_list(
132-
context, evaluate_parameters(
133-
context, composable_node_description.extra_arguments
134-
)
135-
)
136-
]
100+
request = get_composable_node_load_request(composable_node_description, context)
137101
response = self.__rclpy_load_node_client.call(request)
138102
node_name = response.full_node_name if response.full_node_name else request.node_name
139103
if response.success:
@@ -208,3 +172,55 @@ def execute(
208172
None, self._load_in_sequence, self.__composable_node_descriptions, context
209173
)
210174
)
175+
176+
177+
def get_composable_node_load_request(
178+
composable_node_description: ComposableNode,
179+
context: LaunchContext
180+
):
181+
"""Get the request that will be send to the composable node container."""
182+
request = composition_interfaces.srv.LoadNode.Request()
183+
request.package_name = perform_substitutions(
184+
context, composable_node_description.package
185+
)
186+
request.plugin_name = perform_substitutions(
187+
context, composable_node_description.node_plugin
188+
)
189+
if composable_node_description.node_name is not None:
190+
request.node_name = perform_substitutions(
191+
context, composable_node_description.node_name
192+
)
193+
if composable_node_description.node_namespace is not None:
194+
request.node_namespace = perform_substitutions(
195+
context, composable_node_description.node_namespace
196+
)
197+
# request.log_level = perform_substitutions(context, node_description.log_level)
198+
if composable_node_description.remappings is not None:
199+
for from_, to in composable_node_description.remappings:
200+
request.remap_rules.append('{}:={}'.format(
201+
perform_substitutions(context, list(from_)),
202+
perform_substitutions(context, list(to)),
203+
))
204+
global_params = context.launch_configurations.get('ros_params', None)
205+
parameters = []
206+
if global_params is not None:
207+
parameters.append(normalize_parameter_dict(global_params))
208+
if composable_node_description.parameters is not None:
209+
parameters.extend(list(composable_node_description.parameters))
210+
if parameters:
211+
request.parameters = [
212+
param.to_parameter_msg() for param in to_parameters_list(
213+
context, evaluate_parameters(
214+
context, parameters
215+
)
216+
)
217+
]
218+
if composable_node_description.extra_arguments is not None:
219+
request.extra_arguments = [
220+
param.to_parameter_msg() for param in to_parameters_list(
221+
context, evaluate_parameters(
222+
context, composable_node_description.extra_arguments
223+
)
224+
)
225+
]
226+
return request

launch_ros/launch_ros/actions/node.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,6 @@ def __init__(
192192
ensure_argument_type(parameters, (list), 'parameters', 'Node')
193193
# All elements in the list are paths to files with parameters (or substitutions that
194194
# evaluate to paths), or dictionaries of parameters (fields can be substitutions).
195-
i = 0
196-
for param in parameters:
197-
cmd += ['--params-file', LocalSubstitution(
198-
"ros_specific_arguments['params'][{}]".format(i),
199-
description='parameter {}'.format(i))]
200-
i += 1
201195
normalized_params = normalize_parameters(parameters)
202196
if remappings is not None:
203197
i = 0
@@ -220,8 +214,8 @@ def __init__(
220214

221215
self.__expanded_node_name = self.UNSPECIFIED_NODE_NAME
222216
self.__expanded_node_namespace = self.UNSPECIFIED_NODE_NAMESPACE
223-
self.__final_node_name = None # type: Optional[Text]
224217
self.__expanded_parameter_files = None # type: Optional[List[Text]]
218+
self.__final_node_name = None # type: Optional[Text]
225219
self.__expanded_remappings = None # type: Optional[List[Tuple[Text, Text]]]
226220

227221
self.__substitutions_performed = False
@@ -385,13 +379,24 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
385379
if self.__expanded_node_namespace != '/':
386380
self.__final_node_name += self.__expanded_node_namespace
387381
self.__final_node_name += '/' + self.__expanded_node_name
382+
# expand global parameters first,
383+
# so they can be overriden with specific parameters of this Node
384+
global_params = context.launch_configurations.get('ros_params', None)
385+
if global_params is not None or self.__parameters is not None:
386+
self.__expanded_parameter_files = []
387+
if global_params is not None:
388+
param_file_path = self._create_params_file_from_dict(global_params)
389+
self.__expanded_parameter_files.append(param_file_path)
390+
cmd_extension = ['--params-file', f'{param_file_path}']
391+
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
392+
assert os.path.isfile(param_file_path)
388393
# expand parameters too
389394
if self.__parameters is not None:
390-
self.__expanded_parameter_files = []
391395
evaluated_parameters = evaluate_parameters(context, self.__parameters)
392-
for params in evaluated_parameters:
396+
for i, params in enumerate(evaluated_parameters):
393397
if isinstance(params, dict):
394398
param_file_path = self._create_params_file_from_dict(params)
399+
assert os.path.isfile(param_file_path)
395400
elif isinstance(params, pathlib.Path):
396401
param_file_path = str(params)
397402
else:
@@ -400,9 +405,10 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
400405
self.__logger.warning(
401406
'Parameter file path is not a file: {}'.format(param_file_path),
402407
)
403-
# Don't skip adding the file to the parameter list since space has been
404-
# reserved for it in the ros_specific_arguments.
408+
continue
405409
self.__expanded_parameter_files.append(param_file_path)
410+
cmd_extension = ['--params-file', f'{param_file_path}']
411+
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
406412
# expand remappings too
407413
if self.__remappings is not None:
408414
self.__expanded_remappings = []
@@ -425,8 +431,6 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
425431
ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name)
426432
if self.__expanded_node_namespace != '':
427433
ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace)
428-
if self.__expanded_parameter_files is not None:
429-
ros_specific_arguments['params'] = self.__expanded_parameter_files
430434
if self.__expanded_remappings is not None:
431435
ros_specific_arguments['remaps'] = []
432436
for remapping_from, remapping_to in self.__expanded_remappings:

launch_ros/launch_ros/actions/push_ros_namespace.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ def __init__(
5050

5151
@classmethod
5252
def parse(cls, entity: Entity, parser: Parser):
53-
"""Return `SetLaunchConfiguration` action and kwargs for constructing it."""
53+
"""Return `PushRosNamespace` action and kwargs for constructing it."""
5454
_, kwargs = super().parse(entity, parser)
5555
kwargs['namespace'] = parser.parse_substitution(entity.get_attr('namespace'))
5656
return cls, kwargs
5757

5858
@property
5959
def namespace(self) -> List[Substitution]:
60-
"""Getter for self.__name."""
60+
"""Getter for self.__namespace."""
6161
return self.__namespace
6262

6363
def execute(self, context: LaunchContext):
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright 2020 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Module for the `SetParameter` action."""
16+
17+
from typing import List
18+
19+
from launch import Action
20+
from launch import Substitution
21+
from launch.frontend import Entity
22+
from launch.frontend import expose_action
23+
from launch.frontend import Parser
24+
from launch.launch_context import LaunchContext
25+
from launch.some_substitutions_type import SomeSubstitutionsType
26+
27+
from launch_ros.parameters_type import SomeParameterValue
28+
from launch_ros.utilities.evaluate_parameters import evaluate_parameter_dict
29+
from launch_ros.utilities.normalize_parameters import normalize_parameter_dict
30+
31+
32+
@expose_action('set_parameter')
33+
class SetParameter(Action):
34+
"""
35+
Action that sets a parameter in the current context.
36+
37+
This parameter will be set in all the nodes launched in the same scope.
38+
e.g.:
39+
```python3
40+
LaunchDescription([
41+
...,
42+
GroupAction(
43+
actions = [
44+
...,
45+
SetParameter(name='my_param', value='2'),
46+
...,
47+
Node(...), // the param will be passed to this node
48+
...,
49+
]
50+
),
51+
Node(...), // here it won't be passed, as it's not in the same scope
52+
...
53+
])
54+
```
55+
"""
56+
57+
def __init__(
58+
self,
59+
name: SomeSubstitutionsType,
60+
value: SomeParameterValue,
61+
**kwargs
62+
) -> None:
63+
"""Create a SetParameter action."""
64+
super().__init__(**kwargs)
65+
self.__param_dict = normalize_parameter_dict({name: value})
66+
67+
@classmethod
68+
def parse(cls, entity: Entity, parser: Parser):
69+
"""Return `SetParameter` action and kwargs for constructing it."""
70+
_, kwargs = super().parse(entity, parser)
71+
kwargs['name'] = parser.parse_substitution(entity.get_attr('name'))
72+
kwargs['value'] = parser.parse_substitution(entity.get_attr('value'))
73+
return cls, kwargs
74+
75+
@property
76+
def name(self) -> List[Substitution]:
77+
"""Getter for name."""
78+
return self.__param_dict.keys()[0]
79+
80+
@property
81+
def value(self) -> List[Substitution]:
82+
"""Getter for value."""
83+
return self.__param_dict.values()[0]
84+
85+
def execute(self, context: LaunchContext):
86+
"""Execute the action."""
87+
eval_param_dict = evaluate_parameter_dict(context, self.__param_dict)
88+
global_params = context.launch_configurations.get('ros_params', {})
89+
global_params.update(eval_param_dict)
90+
context.launch_configurations['ros_params'] = global_params

launch_ros/launch_ros/utilities/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
__all__ = [
3030
'add_node_name',
3131
'evaluate_parameters',
32+
'evaluate_parameters_dict',
3233
'get_node_name_count',
3334
'normalize_parameters',
35+
'normalize_parameters_dict',
3436
'normalize_remap_rule',
3537
'normalize_remap_rules',
3638
'to_parameters_list',

test_launch_ros/test/test_launch_ros/actions/test_node.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ class TestNode(unittest.TestCase):
3333

3434
def _assert_launch_errors(self, actions):
3535
ld = LaunchDescription(actions)
36-
ls = LaunchService()
36+
ls = LaunchService(debug=True)
3737
ls.include_launch_description(ld)
3838
assert 0 != ls.run()
3939

4040
def _assert_launch_no_errors(self, actions):
4141
ld = LaunchDescription(actions)
42-
ls = LaunchService()
42+
ls = LaunchService(debug=True)
4343
ls.include_launch_description(ld)
4444
assert 0 == ls.run()
4545

0 commit comments

Comments
 (0)