Skip to content

Commit 1d2ef7b

Browse files
committed
[UPDATE] converter to ROS2 launch description
1 parent 1938317 commit 1d2ef7b

File tree

6 files changed

+187
-44
lines changed

6 files changed

+187
-44
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/run_hooks
12
docs/
23
src/*
34
cfg/cpp/

library/aduulm_launch_lib_py/launch_config.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,33 @@ def group(self, group_name: str):
3636
if group_name in data.modules:
3737
group = data.modules[group_name]
3838
if not isinstance(group, LaunchGroup):
39-
raise Exception(f'trying to enter group {group_name}, but it already exists and is not a group! Type: {type(group)}')
39+
raise Exception(
40+
f'trying to enter group {group_name}, but it already exists and is not a group! Type: {type(group)}')
4041
else:
4142
group = LaunchGroup()
4243
data.modules.add(group_name, group)
4344
return LaunchConfig(self.config, self.path + [group_name], group)
4445

4546
def add(self, name: str, child: LeafLaunch):
4647
if name in self.data.modules:
47-
raise Exception(f'trying to add {name} to current group, but something already exists there! {self.data.modules[name]}')
48+
raise Exception(
49+
f'trying to add {name} to current group, but something already exists there! {self.data.modules[name]}')
4850
self.data.modules.add(name, child)
4951

50-
def add_sublaunch_ros(self, name: str, package_name: str, launch_filename: str, **kwargs: Any):
51-
self.add(name, SubLaunchROS_(package_name, launch_filename, args=SaferDict(**kwargs)))
52+
def add_sublaunch_ros(self, name: str, package_name: str, launch_filename: str, args: Dict[str, Any]):
53+
self.add(name, SubLaunchROS_(package_name,
54+
launch_filename, args=SaferDict(**args)))
5255

5356
def exec_sublaunch(self, func: ConfigGeneratorFunc, **args: Any):
5457
func(self, **args)
5558

5659
def exec_sublaunch_lazy(self, name: str, func: ConfigGeneratorFunc, **args: Any):
5760
self.add(name, SubLaunchExecLazy_(func, args=SaferDict(**args)))
5861

59-
def add_node(self, name: str, package_name: str, executable_name: str, remappings: Dict[str, Topic] = {}, **parameters: Any):
60-
self.add(name, RunNode_(package_name, executable_name, remappings=SaferDict(**remappings), parameters=SaferDict(**parameters)))
62+
def add_node(self, name: str, package_name: str, executable_name: str, remappings: Dict[str, Topic] = {},
63+
parameters: Dict[str, Any] = {}, output: str = 'screen', emulate_tty: bool = True):
64+
self.add(name, RunNode_(package_name, executable_name, remappings=SaferDict(**remappings),
65+
parameters=SaferDict(**parameters), output=output, emulate_tty=emulate_tty))
6166

6267
def evaluate(self):
6368
def do_evaluate(config: LaunchConfig, mod: LeafLaunch, name: str, parent: LaunchGroup):
@@ -66,8 +71,9 @@ def do_evaluate(config: LaunchConfig, mod: LeafLaunch, name: str, parent: Launch
6671
mod.func(config, **mod.args)
6772
self.recurse(do_evaluate)
6873

69-
def add_executable(self, name: str, executable_name: str, **args: Any):
70-
self.add(name, Executable_(executable_name, args=SaferDict(**args)))
74+
def add_executable(self, name: str, executable_name: str, args: List[str] = [], output: str = 'screen', emulate_tty: bool = True):
75+
self.add(name, Executable_(executable_name, args=args,
76+
output=output, emulate_tty=emulate_tty))
7177

7278
def enable_all(self):
7379
self.set_enabled(True)
@@ -77,7 +83,8 @@ def disable_all(self):
7783

7884
def recurse(self, cb_leaf: RecurseLeafFunc, cb_enter: Optional[RecurseEnterFunc] = None, cb_exit:
7985
Optional[RecurseExitFunc] = None):
80-
self._do_recurse(self, self.data, None, None, cb_leaf, cb_enter, cb_exit)
86+
self._do_recurse(self, self.data, None, None,
87+
cb_leaf, cb_enter, cb_exit)
8188

8289
def _do_recurse(self, config: LaunchConfig, mod: AnyLaunch, name: Optional[str], parent: Optional[LaunchGroup],
8390
cb_leaf: RecurseLeafFunc, cb_enter: Optional[RecurseEnterFunc] = None, cb_exit: Optional[RecurseExitFunc] = None):
@@ -89,11 +96,13 @@ def _do_recurse(self, config: LaunchConfig, mod: AnyLaunch, name: Optional[str],
8996
cb_enter(config, mod, name, parent)
9097
old_keys = set(mod.modules.keys())
9198
for key in old_keys:
92-
self._do_recurse(config, mod.modules[key], key, mod, cb_leaf, cb_enter)
99+
self._do_recurse(
100+
config, mod.modules[key], key, mod, cb_leaf, cb_enter)
93101
# in case new modules were added
94102
new_keys = set(mod.modules.keys())
95103
for key in new_keys-old_keys:
96-
self._do_recurse(config, mod.modules[key], key, mod, cb_leaf, cb_enter)
104+
self._do_recurse(
105+
config, mod.modules[key], key, mod, cb_leaf, cb_enter)
97106
if name is not None and cb_exit is not None:
98107
cb_exit(old_config, mod, name, parent)
99108
else:
@@ -128,7 +137,9 @@ def __str__(self):
128137

129138

130139
RecurseLeafFunc = Callable[[LaunchConfig, LeafLaunch, str, LaunchGroup], None]
131-
RecurseEnterFunc = Callable[[LaunchConfig, LaunchGroup, str, Optional[LaunchGroup]], None]
132-
RecurseExitFunc = Callable[[LaunchConfig, LaunchGroup, str, Optional[LaunchGroup]], None]
140+
RecurseEnterFunc = Callable[[LaunchConfig,
141+
LaunchGroup, str, Optional[LaunchGroup]], None]
142+
RecurseExitFunc = Callable[[LaunchConfig,
143+
LaunchGroup, str, Optional[LaunchGroup]], None]
133144
P = ParamSpec('P')
134145
ConfigGeneratorFunc = Callable[Concatenate[LaunchConfig, P], None]

library/aduulm_launch_lib_py/types.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
Topic = str
77

8+
89
@dataclass(kw_only=True, slots=True)
910
class Enableable:
1011
enabled: bool = False
@@ -15,89 +16,123 @@ def enable(self):
1516
def disable(self):
1617
self.enabled = False
1718

19+
1820
K = TypeVar('K')
1921
V = TypeVar('V')
22+
23+
2024
class SaferDict(Generic[K, V]):
2125
def __init__(self, **kwargs: V):
2226
self._data: Dict[K, V] = dict(**kwargs)
2327

2428
# only allow __setitem__ if key already exists
2529
def __setitem__(self, key: K, value: V):
2630
if key not in self._data:
27-
raise Exception(f"Could not replace argument {key} because it does not exist!")
31+
raise Exception(
32+
f"Could not replace argument {key} because it does not exist!")
2833
self._data[key] = value
2934

3035
# only allow add if key does not already exist
3136
def add(self, key: K, value: V):
3237
if key in self._data:
33-
raise Exception(f"Could not add argument {key} because it already exists!")
38+
raise Exception(
39+
f"Could not add argument {key} because it already exists!")
3440
self._data[key] = value
3541
return value
3642

3743
# The following methods are proxies for dict methods.
3844
# Maybe these could be replaced by inheriting from dict, but the typing seems to require Python >= 3.12
45+
def todict(self):
46+
return self._data
47+
3948
def __getitem__(self, key: K) -> V:
4049
return self._data[key]
50+
4151
def __iter__(self):
4252
return self._data.__iter__()
53+
4354
def keys(self):
4455
return self._data.keys()
56+
4557
def values(self):
4658
return self._data.values()
59+
4760
def items(self):
4861
return self._data.items()
62+
4963
def __repr__(self):
5064
return repr(self._data)
65+
5166
def __str__(self):
5267
return str(self._data)
68+
5369
def __delitem__(self, key: K):
5470
del self._data[key]
71+
5572
def __eq__(self, other: Any):
5673
if isinstance(other, SaferDict):
5774
return self._data == other._data
5875
return self._data == other
5976

77+
6078
@dataclass(slots=True)
6179
class Executable_(Enableable):
6280
executable_name: str
63-
args: SaferDict[str, Any] = field(default_factory=SaferDict)
81+
args: List[str] = field(default_factory=list)
82+
output: str = 'screen'
83+
emulate_tty: bool = True
84+
6485

6586
@dataclass(slots=True)
6687
class RunNode_(Enableable):
6788
package_name: str
6889
executable_name: str
6990
parameters: SaferDict[str, Any] = field(default_factory=SaferDict)
7091
remappings: SaferDict[str, Topic] = field(default_factory=SaferDict)
92+
output: str = 'screen'
93+
emulate_tty: bool = True
94+
7195

7296
@dataclass(slots=True)
7397
class SubLaunchROS_(Enableable):
7498
package_name: str
7599
launch_filename: str
76100
args: SaferDict[str, Any] = field(default_factory=SaferDict)
77101

102+
78103
ConfigGeneratorFuncAny = Callable[..., None]
104+
105+
79106
@dataclass(slots=True)
80107
class SubLaunchExecLazy_(Enableable):
81108
func: ConfigGeneratorFuncAny
82109
args: SaferDict[str, Any] = field(default_factory=SaferDict)
83110

111+
84112
SubLaunch_ = SubLaunchROS_ | SubLaunchExecLazy_
85113

114+
86115
@dataclass
87116
class LaunchGroup:
88-
modules: SaferDict[str, 'Executable_ | RunNode_ | SubLaunch_ | LaunchGroup'] = field(default_factory=SaferDict)
117+
modules: SaferDict[str, 'Executable_ | RunNode_ | SubLaunch_ | LaunchGroup'] = field(
118+
default_factory=SaferDict)
119+
120+
121+
def Executable(executable_name: str, args: List[Any] = [], output: str = 'screen', emulate_tty: bool = True):
122+
return Executable_(executable_name, args=args, output=output, emulate_tty=emulate_tty)
89123

90-
def Executable(executable_name: str, **kwargs):
91-
return Executable_(executable_name, args=SaferDict(**kwargs))
92124

93-
def RunNode(package_name: str, executable_name: str, remappings: Dict[str, Topic] ={}, **parameters: Any):
94-
return RunNode_(package_name, executable_name, remappings=SaferDict(**remappings), parameters=SaferDict(**parameters))
125+
def RunNode(package_name: str, executable_name: str, remappings: Dict[str, Topic] = {}, parameters: Dict[str, Any] = {}, output: str = 'screen', emulate_tty: bool = True):
126+
return RunNode_(package_name, executable_name, remappings=SaferDict(**remappings), parameters=SaferDict(**parameters), output=output, emulate_tty=emulate_tty)
127+
95128

96129
def SubLaunchROS(package_name: str, launch_filename: str, **args: Any):
97130
return SubLaunchROS_(package_name, launch_filename, args=SaferDict(**args))
98131

132+
99133
def SubLaunchExecLazy(func: ConfigGeneratorFuncAny, **args: Any):
100134
return SubLaunchExecLazy_(func, args=SaferDict(**args))
101135

136+
102137
LeafLaunch = Executable_ | RunNode_ | SubLaunch_
103138
AnyLaunch = LeafLaunch | LaunchGroup

library/test/python/test_launch_config.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
class LaunchConfigTest(unittest.TestCase):
88
def test_simple(self):
99
config = LaunchConfig()
10-
node_args: dict[str, Any] = dict(package_name='test_package', executable_name='test_executable')
10+
node_args: dict[str, Any] = dict(
11+
package_name='test_package', executable_name='test_executable')
1112
params: dict[str, Any] = dict(arg1='value1')
1213
with config.group('test'):
13-
config.add_node(name='test_node', **node_args, **params)
14+
config.add_node(name='test_node', **node_args, parameters=params)
1415
ref = LaunchGroup(modules=SaferDict(
1516
test=LaunchGroup(modules=SaferDict(
16-
test_node=RunNode_(**node_args, parameters=SaferDict(**params), remappings=SaferDict())
17+
test_node=RunNode_(
18+
**node_args, parameters=SaferDict(**params), remappings=SaferDict())
1719
))
1820
))
1921
self.assertEqual(config.data, ref)

ros2/aduulm_launch_py/converter_ros2.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,83 @@
1-
from aduulm_launch_lib_py import LaunchConfig, LaunchGroup, AnyLaunch, SubLaunchROS_, Executable_, RunNode_, SubLaunchExecLazy_
1+
from aduulm_launch_lib_py import LaunchConfig, LaunchGroup, AnyLaunch, SubLaunchROS_, Executable_, RunNode_
22
from typing import Any, List, Optional
33

44
from launch import LaunchDescription
55
from launch.actions import IncludeLaunchDescription, GroupAction, DeclareLaunchArgument, ExecuteProcess
6-
from launch_ros.actions import PushRosNamespace
6+
from launch.launch_description_sources import PythonLaunchDescriptionSource
7+
from launch_ros.actions import PushRosNamespace, Node
8+
from launch.conditions import IfCondition
9+
from launch.substitutions import LaunchConfiguration
10+
from aduulm_tools_python.launch_utils import get_package_share_directory
11+
import os
12+
713

814
def convert_config_to_ros2_launch(config: LaunchConfig):
915
config.evaluate()
10-
1116
modules = []
1217

1318
def recurse(config: LaunchConfig, mod: AnyLaunch, name: Optional[str], modules: List[Any]):
14-
print(mod, name, modules)
1519
if isinstance(mod, LaunchGroup):
1620
if name is not None:
1721
config = config.group(name)
1822
group_modules = []
19-
for key, mod in mod.modules.items():
20-
recurse(config, mod, key, group_modules)
23+
for key, child in mod.modules.items():
24+
recurse(config, child, key, group_modules)
2125
if name is not None:
26+
arg_name = 'launch_' + '__'.join(config.path)
2227
modules.extend([
23-
GroupAction(actions=[
24-
PushRosNamespace(name),
25-
*modules
26-
])
28+
DeclareLaunchArgument(arg_name, default_value='true'),
29+
GroupAction(
30+
actions=[
31+
PushRosNamespace(name),
32+
*group_modules
33+
],
34+
# scoped=False,
35+
# forwarding=False,
36+
condition=IfCondition(LaunchConfiguration(arg_name))
37+
)
2738
])
2839
else:
2940
modules.extend(group_modules)
3041
return
3142
assert isinstance(name, str)
43+
arg_name = 'launch_' + '__'.join(config.path + [name])
44+
arg = DeclareLaunchArgument(arg_name, default_value=str(mod.enabled)),
45+
modules.append(arg)
3246
if isinstance(mod, SubLaunchROS_):
33-
pass
47+
desc = IncludeLaunchDescription(
48+
PythonLaunchDescriptionSource(
49+
os.path.join(
50+
get_package_share_directory(mod.package_name),
51+
'launch',
52+
mod.launch_filename
53+
)
54+
),
55+
launch_arguments=mod.args.items(),
56+
condition=IfCondition(LaunchConfiguration(arg_name))
57+
)
58+
modules.append(desc)
3459
elif isinstance(mod, Executable_):
35-
pass
60+
desc = ExecuteProcess(
61+
name=name,
62+
cmd=[mod.executable_name, *mod.args],
63+
output='screen',
64+
emulate_tty=True,
65+
condition=IfCondition(LaunchConfiguration(arg_name))
66+
)
67+
modules.append(desc)
3668
elif isinstance(mod, RunNode_):
37-
pass
38-
elif isinstance(mod, SubLaunchExecLazy_):
39-
pass
69+
desc = Node(
70+
name=name,
71+
package=mod.package_name,
72+
executable=mod.executable_name,
73+
output='screen',
74+
emulate_tty=True,
75+
parameters=[mod.parameters.todict()],
76+
condition=IfCondition(LaunchConfiguration(arg_name))
77+
)
78+
modules.append(desc)
4079
else:
80+
# SubLaunchExecLazy_ must not occur here, because we called evaluate() beforehand!
4181
assert False
4282

4383
modules = []

0 commit comments

Comments
 (0)