Skip to content

Commit 68bd522

Browse files
Add a way to set remapping rules for all nodes in the same scope (#163) (#203)
Signed-off-by: Ivan Santiago Paunovic <[email protected]> Co-authored-by: Ivan Santiago Paunovic <[email protected]>
1 parent 07c5216 commit 68bd522

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
@@ -200,12 +200,17 @@ def get_composable_node_load_request(
200200
if combined_ns is not None:
201201
request.node_namespace = combined_ns
202202
# request.log_level = perform_substitutions(context, node_description.log_level)
203-
if composable_node_description.remappings is not None:
204-
for from_, to in composable_node_description.remappings:
205-
request.remap_rules.append('{}:={}'.format(
206-
perform_substitutions(context, list(from_)),
207-
perform_substitutions(context, list(to)),
208-
))
203+
remappings = []
204+
global_remaps = context.launch_configurations.get('ros_remaps', None)
205+
if global_remaps:
206+
remappings.extend([f'{src}:={dst}' for src, dst in global_remaps])
207+
if composable_node_description.remappings:
208+
remappings.extend([
209+
f'{perform_substitutions(context, src)}:={perform_substitutions(context, dst)}'
210+
for src, dst in composable_node_description.remappings
211+
])
212+
if remappings:
213+
request.remap_rules = remappings
209214
global_params = context.launch_configurations.get('ros_params', None)
210215
parameters = []
211216
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
@@ -195,14 +194,6 @@ def __init__(
195194
# All elements in the list are paths to files with parameters (or substitutions that
196195
# evaluate to paths), or dictionaries of parameters (fields can be substitutions).
197196
normalized_params = normalize_parameters(parameters)
198-
if remappings is not None:
199-
i = 0
200-
for remapping in normalize_remap_rules(remappings):
201-
k, v = remapping
202-
cmd += ['-r', LocalSubstitution(
203-
"ros_specific_arguments['remaps'][{}]".format(i),
204-
description='remapping {}'.format(i))]
205-
i += 1
206197
# Forward 'exec_name' as to ExecuteProcess constructor
207198
kwargs['name'] = exec_name
208199
super().__init__(cmd=cmd, **kwargs)
@@ -211,7 +202,7 @@ def __init__(
211202
self.__node_name = name
212203
self.__node_namespace = namespace
213204
self.__parameters = [] if parameters is None else normalized_params
214-
self.__remappings = [] if remappings is None else remappings
205+
self.__remappings = [] if remappings is None else list(normalize_remap_rules(remappings))
215206
self.__arguments = arguments
216207

217208
self.__expanded_node_name = self.UNSPECIFIED_NODE_NAME
@@ -401,12 +392,21 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
401392
cmd_extension = ['--params-file', f'{param_file_path}']
402393
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
403394
# expand remappings too
404-
if self.__remappings is not None:
395+
global_remaps = context.launch_configurations.get('ros_remaps', None)
396+
if global_remaps or self.__remappings:
405397
self.__expanded_remappings = []
406-
for k, v in self.__remappings:
407-
key = perform_substitutions(context, normalize_to_list_of_substitutions(k))
408-
value = perform_substitutions(context, normalize_to_list_of_substitutions(v))
409-
self.__expanded_remappings.append((key, value))
398+
if global_remaps:
399+
self.__expanded_remappings.extend(global_remaps)
400+
if self.__remappings:
401+
self.__expanded_remappings.extend([
402+
(perform_substitutions(context, src), perform_substitutions(context, dst))
403+
for src, dst in self.__remappings
404+
])
405+
if self.__expanded_remappings:
406+
cmd_extension = []
407+
for src, dst in self.__expanded_remappings:
408+
cmd_extension.extend(['-r', f'{src}:={dst}'])
409+
self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
410410

411411
def execute(self, context: LaunchContext) -> Optional[List[Action]]:
412412
"""
@@ -422,13 +422,6 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
422422
ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name)
423423
if self.__expanded_node_namespace != '':
424424
ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace)
425-
if self.__expanded_remappings is not None:
426-
ros_specific_arguments['remaps'] = []
427-
for remapping_from, remapping_to in self.__expanded_remappings:
428-
remap_arguments = cast(List[str], ros_specific_arguments['remaps'])
429-
remap_arguments.append(
430-
'{}:={}'.format(remapping_from, remapping_to)
431-
)
432425
context.extend_locals({'ros_specific_arguments': ros_specific_arguments})
433426
ret = super().execute(context)
434427

@@ -448,3 +441,8 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
448441
def expanded_node_namespace(self):
449442
"""Getter for expanded_node_namespace."""
450443
return self.__expanded_node_namespace
444+
445+
@property
446+
def expanded_remapping_rules(self):
447+
"""Getter for expanded_remappings."""
448+
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)