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 16 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
106 changes: 106 additions & 0 deletions launch_ros/examples/lifecycle_pub_sub_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# 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

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."""
def _on_transition_event(context):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not being used FWICT

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right 838242e

print('got transition_event {}'.format(context.locals.event))

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.
event_handler_for_talker_reaching_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.
event_handler_for_talker_reaching_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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for readability of this file I recommend reversing the creation of these actions so we read about them in the order that they would occur

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event handler must be registered before sending the event. Do you have a suggestion for how to reorder it that works?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we are talking about the same scale. What I meant was add the action that creates the node; add the action that configures the node; add the action that activates the node; add the action that starts the listener. But I think it wasn't clear so you thought I was referring to this individual add_action call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checkout 46bd3e1

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 all at once, because the order matters:
ld.add_action(event_handler_for_talker_reaching_inactive_state)
ld.add_action(event_handler_for_talker_reaching_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
# first item is unknown state
self.__current_state = next(iter(ChangeState.valid_states.values()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like it'd be more appropriate to explicitly set self.__current_state = ChangeState.valid_states[lifecycle_msgs.msg.State.PRIMARY_STATE_UNKNOWN] (because we want to do this regardless of dict ordering)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, I think I was imagining that the "first" state might change and this would be more future proof, but that's silly (it was probably late): 6955bc8


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.".format(
self.__rclpy_change_state_client.srv_name))
return
response = self.__rclpy_change_state_client.call(request)
if not response.success:
_logger.warn("Failed to make transition '{}' for LifecycleNode '{}'".format(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this should be an error instead of just a warning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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