Skip to content

Commit

Permalink
Support container in frontend (#235)
Browse files Browse the repository at this point in the history
Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp>
  • Loading branch information
kenji-miyake authored Jun 8, 2021
1 parent 04b3cd3 commit e2abd4b
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
20 changes: 20 additions & 0 deletions launch_ros/launch_ros/actions/composable_node_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from typing import Optional

from launch.action import Action
from launch.frontend import Entity
from launch.frontend import expose_action
from launch.frontend import Parser
from launch.launch_context import LaunchContext
from launch.some_substitutions_type import SomeSubstitutionsType

Expand All @@ -26,6 +29,7 @@
from ..descriptions import ComposableNode


@expose_action('node_container')
class ComposableNodeContainer(Node):
"""Action that executes a container ROS node for composable ROS nodes."""

Expand All @@ -51,6 +55,22 @@ def __init__(
super().__init__(name=name, namespace=namespace, **kwargs)
self.__composable_node_descriptions = composable_node_descriptions

@classmethod
def parse(cls, entity: Entity, parser: Parser):
"""Parse node_container."""
_, kwargs = super().parse(entity, parser)

composable_nodes = entity.get_attr(
'composable_node', data_type=List[Entity], optional=True)
if composable_nodes is not None:
kwargs['composable_node_descriptions'] = []
for entity in composable_nodes:
composable_node_cls, composable_node_kwargs = ComposableNode.parse(parser, entity)
kwargs['composable_node_descriptions'].append(
composable_node_cls(**composable_node_kwargs))

return cls, kwargs

def execute(self, context: LaunchContext) -> Optional[List[Action]]:
"""
Execute the action.
Expand Down
21 changes: 21 additions & 0 deletions launch_ros/launch_ros/actions/load_composable_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import composition_interfaces.srv

from launch.action import Action
from launch.frontend import Entity
from launch.frontend import expose_action
from launch.frontend import Parser
from launch.launch_context import LaunchContext
import launch.logging
from launch.some_substitutions_type import SomeSubstitutionsType
Expand All @@ -46,6 +49,7 @@
from ..utilities.normalize_parameters import normalize_parameter_dict


@expose_action('load_composable_node')
class LoadComposableNodes(Action):
"""Action that loads composable ROS nodes into a running container."""

Expand Down Expand Up @@ -82,6 +86,23 @@ def __init__(
self.__final_target_container_name = None # type: Optional[Text]
self.__logger = launch.logging.get_logger(__name__)

@classmethod
def parse(cls, entity: Entity, parser: Parser):
"""Parse load_composable_node."""
_, kwargs = super().parse(entity, parser)

kwargs['target_container'] = parser.parse_substitution(
entity.get_attr('target', data_type=str))

composable_nodes = entity.get_attr('composable_node', data_type=List[Entity])
kwargs['composable_node_descriptions'] = []
for entity in composable_nodes:
composable_node_cls, composable_node_kwargs = ComposableNode.parse(parser, entity)
kwargs['composable_node_descriptions'].append(
composable_node_cls(**composable_node_kwargs))

return cls, kwargs

def _load_node(
self,
request: composition_interfaces.srv.LoadNode.Request,
Expand Down
48 changes: 48 additions & 0 deletions launch_ros/launch_ros/descriptions/composable_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from typing import List
from typing import Optional

from launch.frontend import Entity
from launch.frontend import Parser
from launch.some_substitutions_type import SomeSubstitutionsType
from launch.substitution import Substitution
# from launch.utilities import ensure_argument_type
Expand Down Expand Up @@ -76,6 +78,52 @@ def __init__(
if extra_arguments:
self.__extra_arguments = normalize_parameters(extra_arguments)

@classmethod
def parse(cls, parser: Parser, entity: Entity):
"""Parse composable_node."""
from launch_ros.actions import Node
kwargs = {}

kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg'))
kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin'))
kwargs['name'] = parser.parse_substitution(entity.get_attr('name'))

namespace = entity.get_attr('namespace', optional=True)
if namespace is not None:
kwargs['namespace'] = parser.parse_substitution(namespace)

parameters = entity.get_attr('param', data_type=List[Entity], optional=True)
if parameters is not None:
kwargs['parameters'] = Node.parse_nested_parameters(parameters, parser)

remappings = entity.get_attr('remap', data_type=List[Entity], optional=True)
if remappings is not None:
kwargs['remappings'] = [
(
parser.parse_substitution(remap.get_attr('from')),
parser.parse_substitution(remap.get_attr('to'))
) for remap in remappings
]

for remap in remappings:
remap.assert_entity_completely_parsed()

extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True)
if extra_arguments is not None:
kwargs['extra_arguments'] = [
{
tuple(parser.parse_substitution(extra_arg.get_attr('name'))):
parser.parse_substitution(extra_arg.get_attr('value'))
} for extra_arg in extra_arguments
]

for extra_arg in extra_arguments:
extra_arg.assert_entity_completely_parsed()

entity.assert_entity_completely_parsed()

return cls, kwargs

@property
def package(self) -> List[Substitution]:
"""Get node package name as a sequence of substitutions to be performed."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Copyright 2019 Open Source Robotics Foundation, Inc.
# Copyright 2020 Open Avatar Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
import io
import textwrap

from launch import LaunchService
from launch.frontend import Parser
from launch.utilities import perform_substitutions
from launch_ros.utilities import evaluate_parameters
import osrf_pycommon.process_utils


def test_launch_component_container_yaml():
yaml_file = textwrap.dedent(
r"""
launch:
- node_container:
pkg: rclcpp_components
exec: component_container
name: my_container
namespace: ''
args: 'test_args'
composable_node:
- pkg: composition
plugin: composition::Talker
name: talker
namespace: test_namespace
remap:
- from: chatter
to: /remap/chatter
param:
- name: use_sim_time
value: true
extra_arg:
- name: use_intra_process_comms
value: 'true'
- load_composable_node:
target: my_container
composable_node:
- pkg: composition
plugin: composition::Listener
name: listener
namespace: test_namespace
remap:
- from: chatter
to: /remap/chatter
param:
- name: use_sim_time
value: true
extra_arg:
- name: use_intra_process_comms
value: 'true'
"""
)
with io.StringIO(yaml_file) as f:
check_launch_component_container(f)


def test_launch_component_container_xml():
xml_file = textwrap.dedent(
r"""
<launch>
<node_container pkg="rclcpp_components" exec="component_container" name="my_container" namespace="" args="test_args">
<composable_node pkg="composition" plugin="composition::Talker" name="talker" namespace="test_namespace">
<remap from="chatter" to="/remap/chatter" />
<param name="use_sim_time" value="true"/>
<extra_arg name="use_intra_process_comms" value="true"/>
</composable_node>
</node_container>
<load_composable_node target="my_container">
<composable_node pkg="composition" plugin="composition::Listener" name="listener" namespace="test_namespace">
<remap from="chatter" to="/remap/chatter" />
<param name="use_sim_time" value="true"/>
<extra_arg name="use_intra_process_comms" value="true"/>
</composable_node>
</load_composable_node>
</launch>
""" # noqa: E501
)
with io.StringIO(xml_file) as f:
check_launch_component_container(f)


def check_launch_component_container(file):
root_entity, parser = Parser.load(file)
ld = parser.parse_description(root_entity)
ls = LaunchService()
ls.include_launch_description(ld)

loop = osrf_pycommon.process_utils.get_loop()
launch_task = loop.create_task(ls.run_async())

node_container, load_composable_node = ld.describe_sub_entities()
talker = node_container._ComposableNodeContainer__composable_node_descriptions[0]
listener = load_composable_node._LoadComposableNodes__composable_node_descriptions[0]

def perform(substitution):
return perform_substitutions(ls.context, substitution)

# Check container params
assert perform(node_container._Node__package) == 'rclcpp_components'
assert perform(node_container._Node__node_executable) == 'component_container'
assert perform(node_container._Node__node_name) == 'my_container'
assert perform(node_container._Node__node_namespace) == ''
assert perform(node_container._Node__arguments[0]) == 'test_args'

assert perform(load_composable_node._LoadComposableNodes__target_container) == 'my_container'

# Check node parameters
talker_remappings = list(talker._ComposableNode__remappings)
listener_remappings = list(listener._ComposableNode__remappings)

talker_params = evaluate_parameters(ls.context, talker._ComposableNode__parameters)
listener_params = evaluate_parameters(ls.context, listener._ComposableNode__parameters)

talker_extra_args = evaluate_parameters(ls.context, talker._ComposableNode__extra_arguments)
listener_extra_args = evaluate_parameters(
ls.context, listener._ComposableNode__extra_arguments)

assert perform(talker._ComposableNode__package) == 'composition'
assert perform(talker._ComposableNode__node_plugin) == 'composition::Talker'
assert perform(talker._ComposableNode__node_name) == 'talker'
assert perform(talker._ComposableNode__node_namespace) == 'test_namespace'
assert (perform(talker_remappings[0][0]),
perform(talker_remappings[0][1])) == ('chatter', '/remap/chatter')
assert talker_params[0]['use_sim_time'] is True

assert perform(listener._ComposableNode__package) == 'composition'
assert perform(listener._ComposableNode__node_plugin) == 'composition::Listener'
assert perform(listener._ComposableNode__node_name) == 'listener'
assert perform(listener._ComposableNode__node_namespace) == 'test_namespace'
assert (perform(listener_remappings[0][0]),
perform(listener_remappings[0][1])) == ('chatter', '/remap/chatter')
assert listener_params[0]['use_sim_time'] is True

# Check extra arguments
assert talker_extra_args[0]['use_intra_process_comms'] is True
assert listener_extra_args[0]['use_intra_process_comms'] is True

timeout_sec = 5
loop.run_until_complete(asyncio.sleep(timeout_sec))
if not launch_task.done():
loop.create_task(ls.shutdown())
loop.run_until_complete(launch_task)
assert 0 == launch_task.result()

0 comments on commit e2abd4b

Please sign in to comment.