Skip to content

Commit b845504

Browse files
ivanpaunojacobperron
authored andcommitted
Add a way to set remapping rules for all nodes in the same scope (#163)
Signed-off-by: Ivan Santiago Paunovic <[email protected]>
1 parent c3b3be0 commit b845504

File tree

5 files changed

+231
-28
lines changed

5 files changed

+231
-28
lines changed

launch_ros/launch_ros/actions/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from .node import Node
2121
from .push_ros_namespace import PushRosNamespace
2222
from .set_parameter import SetParameter
23+
from .set_remap import SetRemap
24+
2325

2426
__all__ = [
2527
'ComposableNodeContainer',
@@ -28,4 +30,5 @@
2830
'Node',
2931
'PushRosNamespace',
3032
'SetParameter',
33+
'SetRemap',
3134
]

launch_ros/launch_ros/actions/load_composable_nodes.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,17 @@ def get_composable_node_load_request(
195195
context, composable_node_description.node_namespace
196196
)
197197
# 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-
))
198+
remappings = []
199+
global_remaps = context.launch_configurations.get('ros_remaps', None)
200+
if global_remaps:
201+
remappings.extend([f'{src}:={dst}' for src, dst in global_remaps])
202+
if composable_node_description.remappings:
203+
remappings.extend([
204+
f'{perform_substitutions(context, src)}:={perform_substitutions(context, dst)}'
205+
for src, dst in composable_node_description.remappings
206+
])
207+
if remappings:
208+
request.remap_rules = remappings
204209
global_params = context.launch_configurations.get('ros_params', None)
205210
parameters = []
206211
if global_params is not None:

launch_ros/launch_ros/actions/node.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import os
1818
import pathlib
1919
from tempfile import NamedTemporaryFile
20-
from typing import cast
2120
from typing import Dict
2221
from typing import Iterable
2322
from typing import List
@@ -193,14 +192,6 @@ def __init__(
193192
# All elements in the list are paths to files with parameters (or substitutions that
194193
# evaluate to paths), or dictionaries of parameters (fields can be substitutions).
195194
normalized_params = normalize_parameters(parameters)
196-
if remappings is not None:
197-
i = 0
198-
for remapping in normalize_remap_rules(remappings):
199-
k, v = remapping
200-
cmd += ['-r', LocalSubstitution(
201-
"ros_specific_arguments['remaps'][{}]".format(i),
202-
description='remapping {}'.format(i))]
203-
i += 1
204195
# Forward 'exec_name' as to ExecuteProcess constructor
205196
kwargs['name'] = exec_name
206197
super().__init__(cmd=cmd, **kwargs)
@@ -209,7 +200,7 @@ def __init__(
209200
self.__node_name = name
210201
self.__node_namespace = namespace
211202
self.__parameters = [] if parameters is None else normalized_params
212-
self.__remappings = [] if remappings is None else remappings
203+
self.__remappings = [] if remappings is None else list(normalize_remap_rules(remappings))
213204
self.__arguments = arguments
214205

215206
self.__expanded_node_name = self.UNSPECIFIED_NODE_NAME
@@ -410,12 +401,21 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
410401
cmd_extension = ['--params-file', f'{param_file_path}']
411402
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
412403
# expand remappings too
413-
if self.__remappings is not None:
404+
global_remaps = context.launch_configurations.get('ros_remaps', None)
405+
if global_remaps or self.__remappings:
414406
self.__expanded_remappings = []
415-
for k, v in self.__remappings:
416-
key = perform_substitutions(context, normalize_to_list_of_substitutions(k))
417-
value = perform_substitutions(context, normalize_to_list_of_substitutions(v))
418-
self.__expanded_remappings.append((key, value))
407+
if global_remaps:
408+
self.__expanded_remappings.extend(global_remaps)
409+
if self.__remappings:
410+
self.__expanded_remappings.extend([
411+
(perform_substitutions(context, src), perform_substitutions(context, dst))
412+
for src, dst in self.__remappings
413+
])
414+
if self.__expanded_remappings:
415+
cmd_extension = []
416+
for src, dst in self.__expanded_remappings:
417+
cmd_extension.extend(['-r', f'{src}:={dst}'])
418+
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
419419

420420
def execute(self, context: LaunchContext) -> Optional[List[Action]]:
421421
"""
@@ -431,13 +431,6 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
431431
ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name)
432432
if self.__expanded_node_namespace != '':
433433
ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace)
434-
if self.__expanded_remappings is not None:
435-
ros_specific_arguments['remaps'] = []
436-
for remapping_from, remapping_to in self.__expanded_remappings:
437-
remap_arguments = cast(List[str], ros_specific_arguments['remaps'])
438-
remap_arguments.append(
439-
'{}:={}'.format(remapping_from, remapping_to)
440-
)
441434
context.extend_locals({'ros_specific_arguments': ros_specific_arguments})
442435
ret = super().execute(context)
443436

@@ -457,3 +450,8 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
457450
def expanded_node_namespace(self):
458451
"""Getter for expanded_node_namespace."""
459452
return self.__expanded_node_namespace
453+
454+
@property
455+
def expanded_remapping_rules(self):
456+
"""Getter for expanded_remappings."""
457+
return self.__expanded_remappings
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 `SetRemap` 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+
from launch.utilities import normalize_to_list_of_substitutions
27+
from launch.utilities import perform_substitutions
28+
29+
30+
@expose_action('set_remap')
31+
class SetRemap(Action):
32+
"""
33+
Action that sets a remapping rule in the current context.
34+
35+
This remapping rule will be passed to all the nodes launched in the same scope, overriding
36+
the ones specified in the `Node` action constructor.
37+
e.g.:
38+
```python3
39+
LaunchDescription([
40+
...,
41+
GroupAction(
42+
actions = [
43+
...,
44+
SetRemap(src='asd', dst='bsd'),
45+
...,
46+
Node(...), // the remap rule will be passed to this node
47+
...,
48+
]
49+
),
50+
Node(...), // here it won't be passed, as it's not in the same scope
51+
...
52+
])
53+
```
54+
"""
55+
56+
def __init__(
57+
self,
58+
src: SomeSubstitutionsType,
59+
dst: SomeSubstitutionsType,
60+
**kwargs
61+
) -> None:
62+
"""Create a SetRemap action."""
63+
super().__init__(**kwargs)
64+
self.__src = normalize_to_list_of_substitutions(src)
65+
self.__dst = normalize_to_list_of_substitutions(dst)
66+
67+
@classmethod
68+
def parse(cls, entity: Entity, parser: Parser):
69+
"""Return `SetRemap` action and kwargs for constructing it."""
70+
_, kwargs = super().parse(entity, parser)
71+
kwargs['src'] = parser.parse_substitution(entity.get_attr('from'))
72+
kwargs['dst'] = parser.parse_substitution(entity.get_attr('to'))
73+
return cls, kwargs
74+
75+
@property
76+
def src(self) -> List[Substitution]:
77+
"""Getter for src."""
78+
return self.__src
79+
80+
@property
81+
def dst(self) -> List[Substitution]:
82+
"""Getter for dst."""
83+
return self.__dst
84+
85+
def execute(self, context: LaunchContext):
86+
"""Execute the action."""
87+
src = perform_substitutions(context, self.__src)
88+
dst = perform_substitutions(context, self.__dst)
89+
global_remaps = context.launch_configurations.get('ros_remaps', [])
90+
global_remaps.append((src, dst))
91+
context.launch_configurations['ros_remaps'] = global_remaps
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
"""Tests for the SetRemap Action."""
16+
17+
from launch import LaunchContext
18+
from launch.actions import PopLaunchConfigurations
19+
from launch.actions import PushLaunchConfigurations
20+
21+
from launch_ros.actions import Node
22+
from launch_ros.actions import SetRemap
23+
from launch_ros.actions.load_composable_nodes import get_composable_node_load_request
24+
from launch_ros.descriptions import ComposableNode
25+
26+
import pytest
27+
28+
29+
class MockContext:
30+
31+
def __init__(self):
32+
self.launch_configurations = {}
33+
34+
def perform_substitution(self, sub):
35+
return sub.perform(None)
36+
37+
38+
def get_set_remap_test_remaps():
39+
return [
40+
pytest.param(
41+
[('from', 'to')],
42+
id='One remapping rule'
43+
),
44+
pytest.param(
45+
[('from1', 'to1'), ('from2', 'to2')],
46+
id='Two remapping rules'
47+
),
48+
]
49+
50+
51+
@pytest.mark.parametrize(
52+
'remapping_rules',
53+
get_set_remap_test_remaps()
54+
)
55+
def test_set_remap(remapping_rules):
56+
lc = MockContext()
57+
for src, dst in remapping_rules:
58+
SetRemap(src, dst).execute(lc)
59+
assert lc.launch_configurations == {'ros_remaps': remapping_rules}
60+
61+
62+
def test_set_remap_is_scoped():
63+
lc = LaunchContext()
64+
push_conf = PushLaunchConfigurations()
65+
pop_conf = PopLaunchConfigurations()
66+
set_remap = SetRemap('from', 'to')
67+
68+
push_conf.execute(lc)
69+
set_remap.execute(lc)
70+
assert lc.launch_configurations == {'ros_remaps': [('from', 'to')]}
71+
pop_conf.execute(lc)
72+
assert lc.launch_configurations == {}
73+
74+
75+
def test_set_remap_with_node():
76+
lc = MockContext()
77+
node = Node(
78+
package='asd',
79+
executable='bsd',
80+
name='my_node',
81+
namespace='my_ns',
82+
remappings=[('from2', 'to2')]
83+
)
84+
set_remap = SetRemap('from1', 'to1')
85+
set_remap.execute(lc)
86+
node._perform_substitutions(lc)
87+
assert len(node.expanded_remapping_rules) == 2
88+
assert node.expanded_remapping_rules == [('from1', 'to1'), ('from2', 'to2')]
89+
90+
91+
def test_set_remap_with_composable_node():
92+
lc = MockContext()
93+
node_description = ComposableNode(
94+
package='asd',
95+
plugin='my_plugin',
96+
name='my_node',
97+
namespace='my_ns',
98+
remappings=[('from2', 'to2')]
99+
)
100+
set_remap = SetRemap('from1', 'to1')
101+
set_remap.execute(lc)
102+
request = get_composable_node_load_request(node_description, lc)
103+
remappings = request.remap_rules
104+
assert len(remappings) == 2
105+
assert remappings[0] == 'from1:=to1'
106+
assert remappings[1] == 'from2:=to2'

0 commit comments

Comments
 (0)