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

Add Action CLI #214

Merged
merged 20 commits into from
Apr 14, 2019
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
30 changes: 30 additions & 0 deletions ros2action/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>ros2action</name>
<version>0.6.3</version>
dirk-thomas marked this conversation as resolved.
Show resolved Hide resolved
<description>
The action command for ROS 2 command line tools.
</description>
<maintainer email="dthomas@osrfoundation.org">Dirk Thomas</maintainer>
jacobperron marked this conversation as resolved.
Show resolved Hide resolved
<license>Apache License 2.0</license>

<author email="jacob@openrobotics.org">Jacob Perron</author>

<depend>rclpy</depend>
<depend>ros2cli</depend>

<exec_depend>action_msgs</exec_depend>
<exec_depend>ament_index_python</exec_depend>
<exec_depend>rosidl_runtime_py</exec_depend>

<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<test_depend>test_msgs</test_depend>

<export>
<build_type>ament_python</build_type>
</export>
</package>
Empty file.
136 changes: 136 additions & 0 deletions ros2action/ros2action/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Copyright 2019 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.

import os

from ament_index_python import get_resource
from ament_index_python import get_resources
from ament_index_python import has_resource

import rclpy.action
from ros2cli.node.direct import DirectNode


def _is_action_status_topic(topic_name, action_name):
return action_name + '/_action/status' == topic_name


def get_action_clients_and_servers(*, node, action_name):
action_clients = []
action_servers = []

node_names_and_ns = node.get_node_names_and_namespaces()
for node_name, node_ns in node_names_and_ns:
# Construct fully qualified name
node_fqn = '/'.join(node_ns) + node_name

# Get any action clients associated with the node
client_names_and_types = rclpy.action.get_action_client_names_and_types_by_node(
node,
node_name,
node_ns,
)
for client_name, client_types in client_names_and_types:
if client_name == action_name:
action_clients.append((node_fqn, client_types))

# Get any action servers associated with the node
server_names_and_types = rclpy.action.get_action_server_names_and_types_by_node(
node,
node_name,
node_ns,
)
for server_name, server_types in server_names_and_types:
if server_name == action_name:
action_servers.append((node_fqn, server_types))

return (action_clients, action_servers)


def get_action_names_and_types(*, node):
return rclpy.action.get_action_names_and_types(node)


def get_action_names(*, node):
action_names_and_types = get_action_names_and_types(node=node)
return [n for (n, t) in action_names_and_types]


def get_action_types(package_name):
if not has_resource('packages', package_name):
raise LookupError('Unknown package name')
try:
content, _ = get_resource('rosidl_interfaces', package_name)
except LookupError:
return []
interface_names = content.splitlines()
# TODO(jacobperron) this logic should come from a rosidl related package
# Only return actions in action folder
return list(sorted({
n[7:-7]
for n in interface_names
if n.startswith('action/') and n[-7:] in ('.idl', '.action')}))
jacobperron marked this conversation as resolved.
Show resolved Hide resolved


def get_all_action_types():
all_action_types = {}
for package_name in get_resources('rosidl_interfaces'):
action_types = get_action_types(package_name)
if action_types:
all_action_types[package_name] = action_types
return all_action_types


def get_action_path(package_name, action_name):
action_types = get_action_types(package_name)
if action_name not in action_types:
raise LookupError('Unknown action type')
prefix_path = has_resource('packages', package_name)
# TODO(jacobperron) this logic should come from a rosidl related package
return os.path.join(prefix_path, 'share', package_name, 'action', action_name + '.action')


def action_name_completer(prefix, parsed_args, **kwargs):
"""Callable returning a list of action names."""
with DirectNode(parsed_args) as node:
return get_action_names(node=node)


def action_type_completer(**kwargs):
"""Callable returning a list of action types."""
action_types = []
for package_name, action_names in get_all_action_types().items():
for action_name in action_names:
action_types.append(
'{package_name}/{action_name}'.format_map(locals()))
return action_types


class ActionTypeCompleter:
"""Callable returning a list of action types."""

def __init__(self, *, action_name_key=None):
self.action_name_key = action_name_key

def __call__(self, prefix, parsed_args, **kwargs):
if self.action_name_key is None:
return action_type_completer()

action_name = getattr(parsed_args, self.action_name_key)
with DirectNode(parsed_args) as node:
names_and_types = get_action_names_and_types(node=node)
for n, t in names_and_types:
if n == action_name:
return t
return []
Empty file.
38 changes: 38 additions & 0 deletions ros2action/ros2action/command/action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2019 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.

from ros2cli.command import add_subparsers
from ros2cli.command import CommandExtension
from ros2cli.verb import get_verb_extensions


class ActionCommand(CommandExtension):
"""Various action related sub-commands."""

def add_arguments(self, parser, cli_name):
self._subparser = parser
# Get verb extensions and let them add their arguments and sub-commands
verb_extensions = get_verb_extensions('ros2action.verb')
add_subparsers(parser, cli_name, '_verb', verb_extensions, required=False)

def main(self, *, parser, args):
if not hasattr(args, '_verb'):
# In case no verb was passed
self._subparser.print_help()
return 0

extension = getattr(args, '_verb')

# Call the verb's main method
return extension.main(args=args)
44 changes: 44 additions & 0 deletions ros2action/ros2action/verb/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2017 Open Source Robotics Foundation, Inc.
jacobperron marked this conversation as resolved.
Show resolved Hide resolved
#
# 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.

from ros2cli.plugin_system import PLUGIN_SYSTEM_VERSION
from ros2cli.plugin_system import satisfies_version


class VerbExtension:
"""
The extension point for 'action' verb extensions.

The following properties must be defined:
* `NAME` (will be set to the entry point name)

The following methods must be defined:
* `main`

The following methods can be defined:
* `add_arguments`
"""

NAME = None
EXTENSION_POINT_VERSION = '0.1'

def __init__(self):
super(VerbExtension, self).__init__()
satisfies_version(PLUGIN_SYSTEM_VERSION, '^0.1')

def add_arguments(self, parser, cli_name):
pass

def main(self, *, args):
raise NotImplementedError()
59 changes: 59 additions & 0 deletions ros2action/ros2action/verb/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2019 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.

from ros2action.api import action_name_completer
from ros2action.api import get_action_clients_and_servers
from ros2action.verb import VerbExtension
from ros2cli.node.direct import DirectNode


class InfoVerb(VerbExtension):
"""Print information about an action."""

def add_arguments(self, parser, cli_name):
arg = parser.add_argument(
'action_name',
help="Name of the ROS action to get info (e.g. '/fibonacci')")
arg.completer = action_name_completer
parser.add_argument(
'-t', '--show-types', action='store_true',
help='Additionally show the action type')
parser.add_argument(
'-c', '--count', action='store_true',
help='Only display the number of action clients and action servers')

def main(self, *, args):
with DirectNode(args) as node:
action_clients, action_servers = get_action_clients_and_servers(
node=node,
action_name=args.action_name,
)

print('Action: {}'.format(args.action_name))
print('Action clients: {}'.format(len(action_clients)))
if not args.count:
for client_name, client_types in action_clients:
if args.show_types:
types_formatted = ', '.join(client_types)
print(' {client_name} [{types_formatted}]'.format_map(locals()))
else:
print(' {client_name}'.format_map(locals()))
print('Action servers: {}'.format(len(action_servers)))
if not args.count:
for server_name, server_types in action_servers:
if args.show_types:
types_formatted = ', '.join(server_types)
print(' {server_name} [{types_formatted}]'.format_map(locals()))
else:
print(' {server_name}'.format_map(locals()))
44 changes: 44 additions & 0 deletions ros2action/ros2action/verb/list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2019 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.

from ros2action.api import get_action_names_and_types
from ros2action.verb import VerbExtension
from ros2cli.node.strategy import DirectNode


class ListVerb(VerbExtension):
"""Output a list of action names."""

def add_arguments(self, parser, cli_name):
parser.add_argument(
'-t', '--show-types', action='store_true',
help='Additionally show the action type')
parser.add_argument(
'-c', '--count-actions', action='store_true',
help='Only display the number of actions discovered')

def main(self, *, args):
with DirectNode(args) as node:
action_names_and_types = get_action_names_and_types(node=node)

if args.count_actions:
print(len(action_names_and_types))
return

for name, types in action_names_and_types:
if args.show_types:
types_formatted = ', '.join(types)
print('{name} [{types_formatted}]'.format_map(locals()))
else:
print('{name}'.format_map(locals()))
Loading