Skip to content

Commit

Permalink
Add Action CLI (#214)
Browse files Browse the repository at this point in the history
* Add ros2action package

Contains ros2cli command 'action' with verbs: list and show.
The list verb lists action names for any running action servers and action clients.
The show verb prints the definition for a given action type.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Add 'info' verb to action command

Prints a list of node names that have an action client or server for a given action name.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Use None as argument to test node

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Add TODOs to move action query functions to rclpy (and rcl_action)

The tool shouldn't need to know details about the implementation of actions.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Add dependency to rclpy

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Add 'send_goal' verb to action command

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Migrate message utility functions to rosidl_runtime_py

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Make use of rclpy functions

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Fix lint

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Fix tests

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Fix test

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Add autocompletion to verbs

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Update year

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Expand and validate action name

This also has the side-effect of making the forward slash optional for the action name.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Print goal ID when sendind a goal

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Cancel goal on SIGINT

Wrapped send goal logic in try-finally clause.
This ensures that any active goal will be canceled before the CLI command terminates and also ensure that the ROS node is shutdown.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Fix typos

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Change maintainer

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Move try-except to verb

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Catch expected exceptions only

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
  • Loading branch information
jacobperron committed Apr 14, 2019
1 parent 07640db commit a463030
Show file tree
Hide file tree
Showing 15 changed files with 733 additions and 0 deletions.
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>
<description>
The action command for ROS 2 command line tools.
</description>
<maintainer email="jacob@openrobotics.org">Jacob Perron</maintainer>
<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.
141 changes: 141 additions & 0 deletions ros2action/ros2action/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 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 rclpy.expand_topic_name import expand_topic_name
from rclpy.validate_full_topic_name import validate_full_topic_name
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 = []

expanded_name = expand_topic_name(action_name, node.get_name(), node.get_namespace())
validate_full_topic_name(expanded_name)

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 == expanded_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 == expanded_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')}))


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 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.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()
63 changes: 63 additions & 0 deletions ros2action/ros2action/verb/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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 rclpy
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:
try:
action_clients, action_servers = get_action_clients_and_servers(
node=node,
action_name=args.action_name,
)
except (ValueError, rclpy.exceptions.InvalidTopicNameException) as e:
raise RuntimeError(e)

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

0 comments on commit a463030

Please sign in to comment.