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

Added ROS2 ActionClient w/ limited functionality #125

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Authors
* Pedro Pereira `@MisterOwlPT <https://github.com/MisterOwlPT>`_
* Domenic Rodriguez `@DomenicP <https://github.com/DomenicP>`_
* Ilia Baranov `@iliabaranov <https://github.com/iliabaranov>`_
* Dani Martinez `@danmartzla <https://github.com/danmartzla>`_
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Unreleased
----------

**Added**
* Added ROS 2 action client object with limited capabilities ``roslibpy.ActionClient``.

**Changed**

Expand All @@ -25,7 +26,7 @@ Unreleased

**Added**

* Added a ROS2-compatible header class in ``roslibpy.ros2.Header``.
* Added a ROS 2 compatible header class in ``roslibpy.ros2.Header``.

**Changed**

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ local ROS environment, allowing usage from platforms other than Linux.

The API of **roslibpy** is modeled to closely match that of `roslibjs`_.

ROS1 is fully supported. ROS2 support is still in progress.
ROS 1 is fully supported. ROS 2 support is still in progress.


Main features
Expand Down
7 changes: 7 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ This example is very simplified and uses the :meth:`roslibpy.actionlib.Goal.wait
function to make the code easier to read as an example. A more robust way to handle
results is to hook up to the ``result`` event with a callback.

For action clients to deal with ROS 2 action servers, check the following example:

.. literalinclude :: files/ros2-action-client.py
:language: python

* :download:`ros2-action-client.py <files/ros2-action-client.py>`

Querying ROS API
----------------

Expand Down
34 changes: 34 additions & 0 deletions docs/files/ros2-action-client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import print_function
danmartzla marked this conversation as resolved.
Show resolved Hide resolved
import roslibpy
import time


global result

client = roslibpy.Ros(host="localhost", port=9090)
client.run()

action_client = roslibpy.ActionClient(client,
"/fibonacci",
"custom_action_interfaces/action/Fibonacci")
result = None

def result_callback(msg):
global result
result = msg["result"]

def feedback_callback(msg):
print(f"Action feedback: {msg['partial_sequence']}")

def fail_callback(msg):
print(f"Action failed: {msg}")

goal_id = action_client.send_goal(roslibpy.ActionGoal({"order": 8}),
result_callback,
feedback_callback,
fail_callback)

while result == None:
time.sleep(1)

print("Action result: {}".format(result["sequence"]))
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ local ROS environment, allowing usage from platforms other than Linux.

The API of **roslibpy** is modeled to closely match that of `roslibjs <http://wiki.ros.org/roslibjs>`_.

ROS1 is fully supported. ROS2 support is still in progress.
ROS 1 is fully supported. ROS 2 support is still in progress.

========
Contents
Expand Down
28 changes: 25 additions & 3 deletions src/roslibpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@
Main ROS concepts
=================

ROS1 vs ROS2
ROS 1 vs ROS 2
------------

This library has been tested to work with ROS1. ROS2 should work, but it is still
This library has been tested to work with ROS 1. ROS 2 should work, but it is still
in the works.

One area in which ROS1 and ROS2 differ is in the header interface. To use ROS2, use
One area in which ROS 1 and ROS 2 differ is in the header interface. To use ROS 2, use
the header defined in the `roslibpy.ros2` module.

.. autoclass:: roslibpy.ros2.Header
Expand Down Expand Up @@ -82,6 +82,20 @@ class and are passed around via :class:`Topics <Topic>` using a **publish/subscr
.. autoclass:: ServiceResponse
:members:

Actions
--------

An Action client for ROS 2 Actions can be used by managing goal/feedback/result
messages via :class:`ActionClient <ActionClient>`.

.. autoclass:: ActionClient
:members:
.. autoclass:: ActionGoal
:members:
.. autoclass:: ActionFeedback
:members:
.. autoclass:: ActionResult
:members:

Parameter server
----------------
Expand Down Expand Up @@ -114,6 +128,10 @@ class and are passed around via :class:`Topics <Topic>` using a **publish/subscr
__version__,
)
from .core import (
ActionClient,
ActionFeedback,
ActionGoal,
ActionResult,
Header,
Message,
Param,
Expand All @@ -140,6 +158,10 @@ class and are passed around via :class:`Topics <Topic>` using a **publish/subscr
"Service",
"ServiceRequest",
"ServiceResponse",
"ActionClient",
"ActionGoal",
"ActionFeedback",
"ActionResult",
"Time",
"Topic",
"set_rosapi_timeout",
Expand Down
68 changes: 65 additions & 3 deletions src/roslibpy/comm/comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import json
import logging

from roslibpy.core import Message, MessageEncoder, ServiceResponse
from roslibpy.core import (
ActionFeedback,
ActionResult,
Message,
MessageEncoder,
ServiceResponse,
)

LOGGER = logging.getLogger("roslibpy")

Expand All @@ -22,19 +28,23 @@ def __init__(self, *args, **kwargs):
super(RosBridgeProtocol, self).__init__(*args, **kwargs)
self.factory = None
self._pending_service_requests = {}
self._pending_action_requests = {}
self._message_handlers = {
"publish": self._handle_publish,
"service_response": self._handle_service_response,
"call_service": self._handle_service_request,
"send_action_goal": self._handle_action_request, # TODO: action server
"cancel_action_goal": self._handle_action_cancel, # TODO: action server
"action_feedback": self._handle_action_feedback,
"action_result": self._handle_action_result,
"status": None, # TODO: add handlers for op: status
}
# TODO: add handlers for op: status

def on_message(self, payload):
message = Message(json.loads(payload.decode("utf8")))
handler = self._message_handlers.get(message["op"], None)
if not handler:
raise RosBridgeException('No handler registered for operation "%s"' % message["op"])

handler(message)

def send_ros_message(self, message):
Expand Down Expand Up @@ -106,3 +116,55 @@ def _handle_service_request(self, message):
raise ValueError("Expected service name missing in service request")

self.factory.emit(message["service"], message)

def send_ros_action_goal(self, message, resultback, feedback, errback):
"""Initiate a ROS action request by sending a goal through the ROS Bridge.

Args:
message (:class:`.Message`): ROS Bridge Message containing the action request.
callback: Callback invoked on receiving result.
feedback: Callback invoked when receiving feedback from action server.
errback: Callback invoked on error.
"""
request_id = message["id"]
self._pending_action_requests[request_id] = (resultback, feedback, errback)

json_message = json.dumps(dict(message), cls=MessageEncoder).encode("utf8")
LOGGER.debug("Sending ROS action goal request: %s", json_message)

self.send_message(json_message)

def _handle_action_request(self, message):
if "action" not in message:
raise ValueError("Expected action name missing in action request")
raise RosBridgeException('Action server capabilities not yet implemented')

def _handle_action_cancel(self, message):
if "action" not in message:
raise ValueError("Expected action name missing in action request")
raise RosBridgeException('Action server capabilities not yet implemented')

def _handle_action_feedback(self, message):
if "action" not in message:
raise ValueError("Expected action name missing in action feedback")

request_id = message["id"]
_, feedback, _ = self._pending_action_requests.get(request_id, None)
feedback(ActionFeedback(message["values"]))

def _handle_action_result(self, message):
request_id = message["id"]
action_handlers = self._pending_action_requests.get(request_id, None)

if not action_handlers:
raise RosBridgeException('No handler registered for action request ID: "%s"' % request_id)

resultback, _ , errback = action_handlers
del self._pending_action_requests[request_id]

if "result" in message and message["result"] is False:
if errback:
errback(message["values"])
else:
if resultback:
resultback(ActionResult(message["values"]))
102 changes: 101 additions & 1 deletion src/roslibpy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"Service",
"ServiceRequest",
"ServiceResponse",
"ActionGoal",
"ActionFeedback",
"ActionResult",
"Time",
"Topic",
]
Expand Down Expand Up @@ -50,7 +53,7 @@ def __init__(self, values=None):
class Header(UserDict):
"""Represents a message header of the ROS type std_msgs/Header.

This header is only compatible with ROS1. For ROS2 headers, use :class:`roslibpy.ros2.Header`.
This header is only compatible with ROS 1. For ROS 2 headers, use :class:`roslibpy.ros2.Header`.

"""

Expand Down Expand Up @@ -131,6 +134,33 @@ def __init__(self, values=None):
self.update(values)


class ActionResult(UserDict):
"""Result returned from a action call."""

def __init__(self, values=None):
self.data = {}
if values is not None:
self.update(values)


class ActionFeedback(UserDict):
"""Feedback returned from a action call."""

def __init__(self, values=None):
self.data = {}
if values is not None:
self.update(values)


class ActionGoal(UserDict):
"""Action Goal for an action call."""

def __init__(self, values=None):
self.data = {}
if values is not None:
self.update(values)


class MessageEncoder(json.JSONEncoder):
"""Internal class to serialize some of the core data types into json."""

Expand Down Expand Up @@ -491,6 +521,76 @@ def _service_response_handler(self, request):
self.ros.send_on_ready(call)


class ActionClient(object):
"""Action Client of ROS 2 actions.

Args:
ros (:class:`.Ros`): Instance of the ROS connection.
name (:obj:`str`): Service name, e.g. ``/fibonacci``.
action_type (:obj:`str`): Action type, e.g. ``rospy_tutorials/fibonacci``.
"""

def __init__(self, ros, name, action_type, reconnect_on_close=True):
self.ros = ros
self.name = name
self.action_type = action_type

self._service_callback = None
self._is_advertised = False
self.reconnect_on_close = reconnect_on_close

def send_goal(self, goal, resultback, feedback, errback):
""" Start a service call.

Note:
The action client is non-blocking.

Args:
request (:class:`.ServiceRequest`): Service request.
resultback: Callback invoked on receiving action result.
feedback: Callback invoked on receiving action feedback.
errback: Callback invoked on error.

Returns:
object: goal ID if successfull, otherwise ``None``.
"""
if self._is_advertised:
return

action_goal_id = "send_action_goal:%s:%d" % (self.name, self.ros.id_counter)

message = Message(
{
"op": "send_action_goal",
"id": action_goal_id,
"action": self.name,
"action_type": self.action_type,
"args": dict(goal),
"feedback": True,
}
)

self.ros.call_async_action(message, resultback, feedback, errback)
return action_goal_id

def cancel_goal(self, goal_id):
""" Cancel an ongoing action.
NOTE: Async cancelation is not yet supported on rosbridge (rosbridge_suite issue #909)

Args:
goal_id: Goal ID returned from "send_goal()"
"""
message = Message(
{
"op": "cancel_action_goal",
"id": goal_id,
"action": self.name,
}
)
self.ros.send_on_ready(message)
# Remove message_id from RosBridgeProtocol._pending_action_requests in comms.py?


class Param(object):
"""A ROS parameter.

Expand Down
Loading