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 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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>`_
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Unreleased
----------

**Added**
* Added ROS2 action client object with limited capabilities ``roslibpy.ActionClient``.
danmartzla marked this conversation as resolved.
Show resolved Hide resolved

**Changed**

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 ROS2 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('Action feedback:',msg['partial_sequence'])

def fail_callback(msg):
print('Action failed:',msg)
danmartzla marked this conversation as resolved.
Show resolved Hide resolved

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']))
22 changes: 22 additions & 0 deletions src/roslibpy/__init__.py
Original file line number Diff line number Diff line change
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 ROS2 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"]))
100 changes: 100 additions & 0 deletions 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 @@ -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 ROS2 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
18 changes: 18 additions & 0 deletions src/roslibpy/ros.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,24 @@ def _send_internal(proto):

self.factory.on_ready(_send_internal)

def call_async_action(self, message, resultback, feedback, errback):
"""Send a action request to ROS once the connection is established.

If a connection to ROS is already available, the request is sent immediately.

Args:
message (:class:`.Message`): ROS Bridge Message containing the request.
resultback: Callback invoked on successful execution.
feedback: Callback invoked on receiving action feedback.
errback: Callback invoked on error.
"""

def _send_internal(proto):
proto.send_ros_action_goal(message, resultback, feedback, errback)
return proto

self.factory.on_ready(_send_internal)

def set_status_level(self, level, identifier):
level_message = Message({"op": "set_level", "level": level, "id": identifier})

Expand Down