Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ROS specific launch #75

Merged
merged 24 commits into from
Jun 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions launch_ros/examples/lifecycle_pub_sub_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright 2018 Open Source Robotics Foundation, 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.

"""Launch a lifecycle talker and a lifecycle listener."""

import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) # noqa
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'launch')) # noqa

import launch
import launch.actions
import launch.events

from launch_ros import get_default_launch_description
import launch_ros.actions
import launch_ros.events
import launch_ros.events.lifecycle

import lifecycle_msgs.msg


def main(argv=sys.argv[1:]):
"""Main."""
ld = launch.LaunchDescription()

# Prepare the talker node.
talker_node = launch_ros.actions.LifecycleNode(
node_name='talker',
package='lifecycle', node_executable='lifecycle_talker', output='screen')

# When the talker reaches the 'inactive' state, make it take the 'activate' transition.
register_event_handler_for_talker_reaches_inactive_state = launch.actions.RegisterEventHandler(
launch_ros.event_handlers.OnStateTransition(
target_lifecycle_node=talker_node, goal_state='inactive',
entities=[
launch.actions.LogInfo(
msg="node 'talker' reached the 'inactive' state, 'activating'."),
launch.actions.EmitEvent(event=launch_ros.events.lifecycle.ChangeState(
lifecycle_node_matcher=launch.events.process.matches_action(talker_node),
transition_id=lifecycle_msgs.msg.Transition.TRANSITION_ACTIVATE,
)),
],
)
)

# When the talker node reaches the 'active' state, log a message and start the listener node.
register_event_handler_for_talker_reaches_active_state = launch.actions.RegisterEventHandler(
launch_ros.event_handlers.OnStateTransition(
target_lifecycle_node=talker_node, goal_state='active',
entities=[
launch.actions.LogInfo(
msg="node 'talker' reached the 'active' state, launching 'listener'."),
launch_ros.actions.LifecycleNode(
node_name='listener',
package='lifecycle', node_executable='lifecycle_listener', output='screen'),
],
)
)

# Make the talker node take the 'configure' transition.
emit_event_to_request_that_talker_does_configure_transition = launch.actions.EmitEvent(
event=launch_ros.events.lifecycle.ChangeState(
lifecycle_node_matcher=launch.events.process.matches_action(talker_node),
transition_id=lifecycle_msgs.msg.Transition.TRANSITION_CONFIGURE,
)
)

# Add the actions to the launch description.
# The order they are added reflects the order in which they will be executed.
ld.add_action(register_event_handler_for_talker_reaches_inactive_state)
ld.add_action(register_event_handler_for_talker_reaches_active_state)
ld.add_action(talker_node)
ld.add_action(emit_event_to_request_that_talker_does_configure_transition)

print('Starting introspection of launch description...')
print('')

print(launch.LaunchIntrospector().format_launch_description(ld))

print('')
print('Starting launch of launch description...')
print('')

# ls = LaunchService(argv=argv, debug=True)
ls = launch.LaunchService(argv=argv)
ls.include_launch_description(get_default_launch_description(prefix_output_with_name=False))
ls.include_launch_description(ld)
ls.run()


if __name__ == '__main__':
main()
58 changes: 58 additions & 0 deletions launch_ros/examples/pub_sub_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright 2018 Open Source Robotics Foundation, 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.

"""Launch a talker and a listener."""

import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) # noqa
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'launch')) # noqa

from launch import LaunchDescription
from launch import LaunchIntrospector
from launch import LaunchService

from launch_ros import get_default_launch_description
import launch_ros.actions


def main(argv=sys.argv[1:]):
"""Main."""
ld = LaunchDescription([
launch_ros.actions.Node(
package='demo_nodes_cpp', node_executable='talker', output='screen',
remappings=[('chatter', 'my_chatter')]),
launch_ros.actions.Node(
package='demo_nodes_cpp', node_executable='listener', output='screen',
remappings=[('chatter', 'my_chatter')]),
])

print('Starting introspection of launch description...')
print('')

print(LaunchIntrospector().format_launch_description(ld))

print('')
print('Starting launch of launch description...')
print('')

# ls = LaunchService(debug=True)
ls = LaunchService()
ls.include_launch_description(get_default_launch_description(prefix_output_with_name=False))
ls.include_launch_description(ld)
ls.run()


if __name__ == '__main__':
main()
29 changes: 29 additions & 0 deletions launch_ros/launch_ros/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2018 Open Source Robotics Foundation, 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.

"""Main entry point for the `launch_ros` package."""

from . import actions
from . import event_handlers
from . import events
from . import substitutions
from .default_launch_description import get_default_launch_description

__all__ = [
'actions',
'event_handlers',
'events',
'substitutions',
'get_default_launch_description',
]
23 changes: 23 additions & 0 deletions launch_ros/launch_ros/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2018 Open Source Robotics Foundation, 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.

"""actions Module."""

from .lifecycle_node import LifecycleNode
from .node import Node

__all__ = [
'LifecycleNode',
'Node',
]
123 changes: 123 additions & 0 deletions launch_ros/launch_ros/actions/lifecycle_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright 2018 Open Source Robotics Foundation, 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.

"""Module for the LifecycleNode action."""

import functools
import logging
from typing import cast
from typing import List
from typing import Optional
from typing import Text

import launch
from launch.action import Action

import lifecycle_msgs.msg
import lifecycle_msgs.srv

from .node import Node
from ..events.lifecycle import ChangeState
from ..events.lifecycle import StateTransition

_logger = logging.getLogger(name='launch_ros')


class LifecycleNode(Node):
"""Action that executes a ROS lifecycle node."""

def __init__(self, *, node_name: Text, **kwargs) -> None:
"""
Construct a LifecycleNode action.

Almost all of the arguments are passed to :class:`Node` and eventually
to :class:`launch.actions.ExecuteProcess`, so see the documentation of
those classes for additional details.

This action additionally emits some event(s) in certain circumstances:

- :class:`launch.events.lifecycle.StateTransition`:

- this event is emitted when a message is published to the
"/<node_name>/transition_event" topic, indicating the lifecycle
node represented by this action changed state

This action also handles some events related to lifecycle:

- :class:`launch.events.lifecycle.ChangeState`

- this event can be targeted to a single lifecycle node, or more than
one, or even all lifecycle nodes, and it requests the targeted nodes
to change state, see its documentation for more details.
"""
super().__init__(node_name=node_name, **kwargs)
self.__rclpy_subscription = None
self.__current_state = \
ChangeState.valid_states[lifecycle_msgs.msg.State.PRIMARY_STATE_UNKNOWN]

def _on_transition_event(self, context, msg):
try:
event = StateTransition(action=self, msg=msg)
self.__current_state = ChangeState.valid_states[msg.goal_state.id]
context.emit_event_sync(event)
except Exception as exc:
print("Exception in handling of 'lifecycle.msg.TransitionEvent': {}".format(exc))

def _call_change_state(self, request, context: launch.LaunchContext):
while not self.__rclpy_change_state_client.wait_for_service(timeout_sec=1.0):
if context.is_shutdown:
_logger.warn("Abandoning wait for the '{}' service, due to shutdown.".format(
self.__rclpy_change_state_client.srv_name))
return
response = self.__rclpy_change_state_client.call(request)
if not response.success:
_logger.error("Failed to make transition '{}' for LifecycleNode '{}'".format(
ChangeState.valid_transitions[request.transition.id],
self.node_name,
))

def _on_change_state_event(self, context: launch.LaunchContext) -> None:
typed_event = cast(ChangeState, context.locals.event)
if not typed_event.lifecycle_node_matcher(self):
return None
request = lifecycle_msgs.srv.ChangeState.Request()
request.transition.id = typed_event.transition_id
context.add_completion_future(
context.asyncio_loop.run_in_executor(None, self._call_change_state, request, context))

def execute(self, context: launch.LaunchContext) -> Optional[List[Action]]:
"""
Execute the action.

Delegated to :meth:`launch.actions.ExecuteProcess.execute`.
"""
self._perform_substitutions(context) # ensure self.node_name is expanded
if '<node_name_unspecified>' in self.node_name:
raise RuntimeError('node_name unexpectedly incomplete for lifecycle node')
# Create a subscription to monitor the state changes of the subprocess.
self.__rclpy_subscription = context.locals.launch_ros_node.create_subscription(
lifecycle_msgs.msg.TransitionEvent,
'{}/transition_event'.format(self.node_name),
functools.partial(self._on_transition_event, context))
# Create a service client to change state on demand.
self.__rclpy_change_state_client = context.locals.launch_ros_node.create_client(
lifecycle_msgs.srv.ChangeState,
'{}/change_state'.format(self.node_name))
# Register an event handler to change states on a ChangeState lifecycle event.
context.register_event_handler(launch.EventHandler(
matcher=lambda event: isinstance(event, ChangeState),
entities=[launch.actions.OpaqueFunction(function=self._on_change_state_event)],
))
# Delegate execution to Node and ExecuteProcess.
return super().execute(context)
Loading