Skip to content

Commit 4e9ace0

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

File tree

7 files changed

+315
-55
lines changed

7 files changed

+315
-55
lines changed

launch_ros/launch_ros/actions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
from .lifecycle_node import LifecycleNode
1919
from .load_composable_nodes import LoadComposableNodes
2020
from .node import Node
21+
from .set_parameter import SetParameter
2122

2223
__all__ = [
2324
'ComposableNodeContainer',
2425
'LifecycleNode',
2526
'LoadComposableNodes',
2627
'Node',
28+
'SetParameter',
2729
]

launch_ros/launch_ros/actions/load_composable_nodes.py

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ..descriptions import ComposableNode
3131
from ..utilities import evaluate_parameters
3232
from ..utilities import to_parameters_list
33+
from ..utilities.normalize_parameters import normalize_parameter_dict
3334

3435

3536
class LoadComposableNodes(Action):
@@ -85,44 +86,7 @@ def _load_node(
8586
)
8687
)
8788
return
88-
request = composition_interfaces.srv.LoadNode.Request()
89-
request.package_name = perform_substitutions(
90-
context, composable_node_description.package
91-
)
92-
request.plugin_name = perform_substitutions(
93-
context, composable_node_description.node_plugin
94-
)
95-
if composable_node_description.node_name is not None:
96-
request.node_name = perform_substitutions(
97-
context, composable_node_description.node_name
98-
)
99-
if composable_node_description.node_namespace is not None:
100-
request.node_namespace = perform_substitutions(
101-
context, composable_node_description.node_namespace
102-
)
103-
# request.log_level = perform_substitutions(context, node_description.log_level)
104-
if composable_node_description.remappings is not None:
105-
for from_, to in composable_node_description.remappings:
106-
request.remap_rules.append('{}:={}'.format(
107-
perform_substitutions(context, list(from_)),
108-
perform_substitutions(context, list(to)),
109-
))
110-
if composable_node_description.parameters is not None:
111-
request.parameters = [
112-
param.to_parameter_msg() for param in to_parameters_list(
113-
context, evaluate_parameters(
114-
context, composable_node_description.parameters
115-
)
116-
)
117-
]
118-
if composable_node_description.extra_arguments is not None:
119-
request.extra_arguments = [
120-
param.to_parameter_msg() for param in to_parameters_list(
121-
context, evaluate_parameters(
122-
context, composable_node_description.extra_arguments
123-
)
124-
)
125-
]
89+
request = get_composable_node_load_request(composable_node_description, context)
12690
response = self.__rclpy_load_node_client.call(request)
12791
if not response.success:
12892
self.__logger.error(
@@ -173,3 +137,55 @@ def execute(
173137
None, self._load_in_sequence, self.__composable_node_descriptions, context
174138
)
175139
)
140+
141+
142+
def get_composable_node_load_request(
143+
composable_node_description: ComposableNode,
144+
context: LaunchContext
145+
):
146+
"""Get the request that will be send to the composable node container."""
147+
request = composition_interfaces.srv.LoadNode.Request()
148+
request.package_name = perform_substitutions(
149+
context, composable_node_description.package
150+
)
151+
request.plugin_name = perform_substitutions(
152+
context, composable_node_description.node_plugin
153+
)
154+
if composable_node_description.node_name is not None:
155+
request.node_name = perform_substitutions(
156+
context, composable_node_description.node_name
157+
)
158+
if composable_node_description.node_namespace is not None:
159+
request.node_namespace = perform_substitutions(
160+
context, composable_node_description.node_namespace
161+
)
162+
# request.log_level = perform_substitutions(context, node_description.log_level)
163+
if composable_node_description.remappings is not None:
164+
for from_, to in composable_node_description.remappings:
165+
request.remap_rules.append('{}:={}'.format(
166+
perform_substitutions(context, list(from_)),
167+
perform_substitutions(context, list(to)),
168+
))
169+
global_params = context.launch_configurations.get('ros_params', None)
170+
parameters = []
171+
if global_params is not None:
172+
parameters.append(normalize_parameter_dict(global_params))
173+
if composable_node_description.parameters is not None:
174+
parameters.extend(list(composable_node_description.parameters))
175+
if parameters:
176+
request.parameters = [
177+
param.to_parameter_msg() for param in to_parameters_list(
178+
context, evaluate_parameters(
179+
context, parameters
180+
)
181+
)
182+
]
183+
if composable_node_description.extra_arguments is not None:
184+
request.extra_arguments = [
185+
param.to_parameter_msg() for param in to_parameters_list(
186+
context, evaluate_parameters(
187+
context, composable_node_description.extra_arguments
188+
)
189+
)
190+
]
191+
return request

launch_ros/launch_ros/actions/node.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,6 @@ def __init__(
136136
ensure_argument_type(parameters, (list), 'parameters', 'Node')
137137
# All elements in the list are paths to files with parameters (or substitutions that
138138
# evaluate to paths), or dictionaries of parameters (fields can be substitutions).
139-
i = 0
140-
for param in parameters:
141-
i += 1
142-
cmd += [LocalSubstitution(
143-
'ros_specific_arguments[{}]'.format(ros_args_index),
144-
description='parameter {}'.format(i))]
145-
ros_args_index += 1
146139
normalized_params = normalize_parameters(parameters)
147140
if remappings is not None:
148141
i = 0
@@ -164,8 +157,8 @@ def __init__(
164157

165158
self.__expanded_node_name = '<node_name_unspecified>'
166159
self.__expanded_node_namespace = '/'
167-
self.__final_node_name = None # type: Optional[Text]
168160
self.__expanded_parameter_files = None # type: Optional[List[Text]]
161+
self.__final_node_name = None # type: Optional[Text]
169162
self.__expanded_remappings = None # type: Optional[List[Tuple[Text, Text]]]
170163

171164
self.__substitutions_performed = False
@@ -219,13 +212,24 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
219212
if self.__expanded_node_namespace not in ['', '/']:
220213
self.__final_node_name += self.__expanded_node_namespace
221214
self.__final_node_name += '/' + self.__expanded_node_name
215+
# expand global parameters first,
216+
# so they can be overriden with specific parameters of this Node
217+
global_params = context.launch_configurations.get('ros_params', None)
218+
if global_params is not None or self.__parameters is not None:
219+
self.__expanded_parameter_files = []
220+
if global_params is not None:
221+
param_file_path = self._create_params_file_from_dict(global_params)
222+
self.__expanded_parameter_files.append(param_file_path)
223+
cmd_extension = ['--params-file', f'{param_file_path}']
224+
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
225+
assert os.path.isfile(param_file_path)
222226
# expand parameters too
223227
if self.__parameters is not None:
224-
self.__expanded_parameter_files = []
225228
evaluated_parameters = evaluate_parameters(context, self.__parameters)
226-
for params in evaluated_parameters:
229+
for i, params in enumerate(evaluated_parameters):
227230
if isinstance(params, dict):
228231
param_file_path = self._create_params_file_from_dict(params)
232+
assert os.path.isfile(param_file_path)
229233
elif isinstance(params, pathlib.Path):
230234
param_file_path = str(params)
231235
else:
@@ -234,9 +238,10 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
234238
self.__logger.warning(
235239
'Parameter file path is not a file: {}'.format(param_file_path),
236240
)
237-
# Don't skip adding the file to the parameter list since space has been
238-
# reserved for it in the ros_specific_arguments.
241+
continue
239242
self.__expanded_parameter_files.append(param_file_path)
243+
cmd_extension = ['--params-file', f'{param_file_path}']
244+
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
240245
# expand remappings too
241246
if self.__remappings is not None:
242247
self.__expanded_remappings = []
@@ -259,9 +264,6 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
259264
ros_specific_arguments.append('__node:={}'.format(self.__expanded_node_name))
260265
if self.__node_namespace is not None:
261266
ros_specific_arguments.append('__ns:={}'.format(self.__expanded_node_namespace))
262-
if self.__expanded_parameter_files is not None:
263-
for param_file_path in self.__expanded_parameter_files:
264-
ros_specific_arguments.append('__params:={}'.format(param_file_path))
265267
if self.__expanded_remappings is not None:
266268
for remapping_from, remapping_to in self.__expanded_remappings:
267269
ros_specific_arguments.append('{}:={}'.format(remapping_from, remapping_to))
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
@@ -26,7 +26,9 @@
2626

2727
__all__ = [
2828
'evaluate_parameters',
29+
'evaluate_parameters_dict',
2930
'normalize_parameters',
31+
'normalize_parameters_dict',
3032
'normalize_remap_rule',
3133
'normalize_remap_rules',
3234
'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
@@ -30,13 +30,13 @@ class TestNode(unittest.TestCase):
3030

3131
def _assert_launch_errors(self, actions):
3232
ld = LaunchDescription(actions)
33-
ls = LaunchService()
33+
ls = LaunchService(debug=True)
3434
ls.include_launch_description(ld)
3535
assert 0 != ls.run()
3636

3737
def _assert_launch_no_errors(self, actions):
3838
ld = LaunchDescription(actions)
39-
ls = LaunchService()
39+
ls = LaunchService(debug=True)
4040
ls.include_launch_description(ld)
4141
assert 0 == ls.run()
4242

0 commit comments

Comments
 (0)