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 ros2 action client capability #813

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
63abb1b
added loader for action class and instances
sathak93 Sep 3, 2022
cfd3a95
changes status and goal inst to action_msgs
sathak93 Sep 5, 2022
f617d7e
added initial action client working
sathak93 Sep 5, 2022
a7a4775
added create client capability working
sathak93 Sep 6, 2022
e00d188
added action class to rosbridge _protocol
sathak93 Sep 13, 2022
e0029c4
protocol send improve
sathak93 Sep 13, 2022
7736a28
moved createclient to init
sathak93 Sep 13, 2022
4189200
call in a new thread
sathak93 Sep 17, 2022
28ab4e1
start action in new thread
sathak93 Sep 17, 2022
9b7dbd2
cleanup
sathak93 Sep 20, 2022
450314a
feedback as option
sathak93 Sep 25, 2022
bd847af
changes
sathak93 Sep 25, 2022
bc97e79
fix comma
sathak93 Sep 25, 2022
6a0482e
rename and changes in send msg
sathak93 Oct 5, 2022
5c25b33
cancel goal initial commit working
sathak93 Oct 8, 2022
88551ee
change logic starting thread
sathak93 Oct 8, 2022
c084318
unregister cancecl goal
sathak93 Oct 8, 2022
065d9f0
seperate action client
sathak93 Oct 8, 2022
c7e2863
linting fix
sathak93 Oct 9, 2022
e9345fd
linting cleanup
sathak93 Oct 9, 2022
82e3e43
linting cleanup rename
sathak93 Oct 9, 2022
291ff9e
rename
sathak93 Oct 12, 2022
d5a547c
lint
sathak93 Oct 12, 2022
f3650d9
fix action cancel service
sathak93 Oct 13, 2022
730d54f
added comment
sathak93 Oct 13, 2022
def764b
cancel goal through service call
sathak93 Oct 16, 2022
48851c7
cleanup
sathak93 Oct 16, 2022
0bb8249
revert unrelated changes
sathak93 Oct 16, 2022
f398b27
cancel before destroy
sathak93 Oct 16, 2022
c84b9bb
Merge branch 'RobotWebTools:ros2' into act2
sathak93 Oct 16, 2022
6367827
fix ci precommit errors
sathak93 Oct 16, 2022
18124f0
create Client per name
sathak93 Nov 16, 2022
8b0d2df
get_result instead of async
sathak93 Nov 24, 2022
9c0ca8e
added action spec to protocol
sathak93 Nov 27, 2022
44e78fd
fix missed
sathak93 Nov 28, 2022
d45eb8e
fix topo and json
sathak93 Oct 18, 2023
7dfec71
variable rename
sathak93 Oct 19, 2023
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
48 changes: 48 additions & 0 deletions ROSBRIDGE_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,54 @@ A response to a ROS service call
response will contain the ID
* **result** - return value of service callback. true means success, false failure.

#### 3.3.10 Send Goal

```json
{ "op": "send_goal",
(optional) "id": <string>,
"action_name": <string>,
"action_type": <string>,
(optional) "feedback": <bool>,
(optional) "goal_msg": <list<json>>
}
```

This command creates an action client with the specified action name and type if not already created; sends the goal msg to the action server and returns the feedback and result of the goal.

* **action_name** – the name of the action to which the goal will be sent (ex. /navigate_to_pose)
* **action_type** – the type of the action (ex. /nav2_msgs/action/NavigateToPose)
* **feedback** – an optional boolean specifies whether to receive the feedback for this goal or not. defaults to true)
* **goal_msg** – if the Goal has no args, then goal msg does not have to be
provided, though an empty list is equally acceptable. goal msg should be a list of json objects representing the arguments to the action
* **id** – an optional id to distinguish this goal call

#### 3.3.11 Cancel Goal

```json
{ "op": "cancel_goal",
(optional) "id": <string>,
"action_name": <string>
}
```

This command cancels all the executing and pending goals of the specified action and return the result of cancel call.

* **action_name** – the name of the action (ex. /navigate_to_pose)
* **id** – an optional id to distinguish this cancel goal call.

#### 3.3.12 Destroy Client

```json
{ "op": "destroy_client",
(optional) "id": <string>,
"action_name": <string>
}
```
This command destroys the action client if the action client was created on earlier send_goal calls.

* **action_name** – the name of the action (ex. /navigate_to_pose)
* **id** – an optional id to distinguish this cancel goal call.

## 4 Further considerations

Further considerations for the rosbridge protocol are listed below.
Expand Down
184 changes: 184 additions & 0 deletions rosbridge_library/src/rosbridge_library/capabilities/action_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import fnmatch
from functools import partial

from rosbridge_library.capability import Capability
from rosbridge_library.internal.actions import ActionClientHandle, GoalHandle
from rosbridge_library.internal.message_conversion import extract_values


class ActionClientRequests(Capability):
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem like it's named consistently as a "capability" compared to the others.

I think maybe this should be called SendActionGoal or something?


send_goal_msg_fields = [
(True, "action_name", str),
(True, "action_type", str),
(False, "feedback", bool),
]

destroy_client_msg_fields = [(True, "action_name", str)]

cancel_goal_msg_fields = [(True, "action_name", str)]

actions_glob = None

def __init__(self, protocol):
# Call superclass constructor
Capability.__init__(self, protocol)

# Register the operations that this capability provides
protocol.register_operation("send_goal", self.send_goal)
protocol.register_operation("destroy_client", self.destroy_client)
protocol.register_operation("cancel_goal", self.cancel_goal)

self._action_clients = {}

def send_goal(self, msg):
# Check the args
self.basic_type_check(msg, self.send_goal_msg_fields)
action_name = msg.get("action_name")
action_type = msg.get("action_type")
goal_msg = msg.get("goal_msg", [])
feedback = msg.get("feedback", True)

if ActionClientRequests.actions_glob is not None and ActionClientRequests.actions_glob:
self.protocol.log(
"debug", "Action security glob enabled, checking action clients: " + action_name
)
match = False
for glob in ActionClientRequests.actions_glob:
if fnmatch.fnmatch(action_name, glob):
self.protocol.log(
"debug",
"Found match with glob " + glob + ", creating Action client",
)
match = True
break
if not match:
self.protocol.log(
"warn",
"No match found for action, cancelling creation of action client: "
+ action_name,
)
return
else:
self.protocol.log("debug", "No action security glob, not checking Action client.")

client_id = msg.get("id", None)
s_cb = partial(self._success, client_id, action_name)
e_cb = partial(self._failure, client_id, action_name)
if feedback:
f_cb = partial(self._feedback, client_id, action_name)
else:
f_cb = None

if action_name not in self._action_clients:
self._action_clients[action_name] = ActionClientHandle(
action_name, action_type, self.protocol.node_handle
)

GoalHandle(self._action_clients[action_name], goal_msg, s_cb, e_cb, f_cb).start()

def _success(self, cid, action_name, message):
outgoing_message = {
"op": "action_response",
"response_type": "result",
"name": action_name,
"values": message,
}
if cid is not None:
outgoing_message["id"] = cid
# TODO: fragmentation, compression
self.protocol.send(outgoing_message)

def _failure(self, cid, action_name, exc):
outgoing_message = {
"op": "action_response",
"response_type": "error",
"name": action_name,
"values": str(exc),
}
if cid is not None:
outgoing_message["id"] = cid
# TODO: fragmentation, compression
self.protocol.send(outgoing_message)

def _feedback(self, cid, action_name, message):
outgoing_message = {
"op": "action_response",
"response_type": "feedback",
"name": action_name,
"values": extract_values(message),
}
if cid is not None:
outgoing_message["id"] = cid
# TODO: fragmentation, compression
self.protocol.send(outgoing_message)

def destroy_client(self, msg):
self.basic_type_check(msg, self.destroy_client_msg_fields)
action_name = msg.get("action_name")

if ActionClientRequests.actions_glob is not None and ActionClientRequests.actions_glob:
self.protocol.log(
"debug",
"Action security glob enabled, checking action clients: " + action_name,
)
match = False
for glob in ActionClientRequests.actions_glob:
if fnmatch.fnmatch(action_name, glob):
self.protocol.log(
"debug",
"Found match with glob " + glob + ", killing client",
)
match = True
break
if not match:
self.protocol.log(
"warn",
"No match found for action client, cancelling destruction of action client: "
+ action_name,
)
return
else:
self.protocol.log("debug", "No action security glob, not checking Action client.")

if action_name not in self._action_clients:
self.protocol.log("info", "action client %s not available" % action_name)
return
self._action_clients[action_name].unregister()
del self._action_clients[action_name]

if len(self._action_clients) == 0:
self._action_clients.clear()

self.protocol.log("info", "Destroyed Action Client %s" % action_name)

def cancel_goal(self, msg):
self.basic_type_check(msg, self.cancel_goal_msg_fields)
action_name = msg.get("action_name")
cid = msg.get("id", None)

if action_name not in self._action_clients:
self.protocol.log("info", "action client %s not available" % action_name)
return

result = self._action_clients[action_name].cancel_goal_call()

outgoing_message = {
"op": "action_response",
"response_type": "cancel",
"name": action_name,
"values": result,
}
if cid is not None:
outgoing_message["id"] = cid
self.protocol.send(outgoing_message)

self.protocol.log("info", "cancelled goals of %s" % action_name)

def finish(self):
for clients in self._action_clients.values():
clients.unregister()
self._action_clients.clear()
self.protocol.unregister_operation("send_goal")
self.protocol.unregister_operation("destroy_client")
self.protocol.unregister_operation("cancel_goal")
156 changes: 156 additions & 0 deletions rosbridge_library/src/rosbridge_library/internal/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
from threading import Thread

import rclpy
from action_msgs.msg import GoalStatus
from action_msgs.srv import CancelGoal
from rclpy.action import ActionClient
from rosbridge_library.internal.message_conversion import (
extract_values,
populate_instance,
)
from rosbridge_library.internal.ros_loader import (
get_action_class,
get_action_goal_instance,
)
from unique_identifier_msgs.msg import UUID


class InvalidActionException(Exception):
def __init__(self, actiontype):
Exception.__init__(self, "Action %s does not exist" % actiontype)


class ActionClientHandle:
def __init__(self, action_name, action_type, node_handle):
self.action_name = action_name
self.action_type = action_type
self.node_handle = node_handle

# raise exception if action type specified is None
if self.action_type is None:
raise InvalidActionException(action_type)

# loads the class of action type
action_class = get_action_class(action_type)
self.action_client = ActionClient(node_handle, action_class, action_name)
self.node_handle.get_logger().info(
f" Created Action Client: {action_name} of type: {action_type}"
)

# create a client for cancel goal service call
self.cancel_client = self.node_handle.create_client(
CancelGoal, self.action_name + "/_action/cancel_goal"
)

def cancel_goal_call(self):
"""
Sends a cancel goal service call.It cancels all active and pending goals
of the action server by providing zeros to both goal id and stamp.
"""
msg = CancelGoal.Request()
msg.goal_info.goal_id = UUID(uuid=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
msg.goal_info.stamp.nanosec = 0
msg.goal_info.stamp.sec = 0

# create a call with the cancel request
result = self.cancel_client.call(msg)
if result is not None:
# Turn the response into JSON and pass to the callback
json_response = extract_values(result)
else:
raise Exception(result)

return json_response

def unregister(self):
# cancel goals if any before destroy the client
self.cancel_goal_call()
self.cancel_client.destroy()
self.action_client.destroy()


class GoalHandle(Thread):
def __init__(
self, action_client, goal_msg, success_callback, error_callback, feedback_callback
):
"""
Create a goal handle for the specified action.
Use start() to start in a separate thread or run() to run in this thread.

Keyword Arguments:
-----------------
action_client -- the action client on which the goal is handled
goal_msg -- arguments to pass to the action. Can be an
ordered list, or a dict of name-value pairs. Anything else will be
treated as though no arguments were provided (which is still valid for
some kinds of action).
success_callback -- a callback to call with the JSON result of the
goal.
error_callback -- a callback to call if an error occurs. The
callback will be passed the exception that caused the failure of goal.
feedback_callback -- a callback to call with the feedback while the goal is executing if opted.
"""
Thread.__init__(self)
self.daemon = True
self.goal_msg = goal_msg
self.success = success_callback
self.error = error_callback
self.feedback = feedback_callback
self.client = action_client

def run(self):
try:
# Call the action and pass the result to the success handler
self.success(self.start_goal(self.goal_msg))
except Exception as e:
# On error, just pass the exception to the error handler
self.error(e)

def args_to_action_goal_instance(self, inst, args):
"""
Populate a action goal instance with the provided args.
args can be a dictionary of values, or a list, or None
Propagates any exceptions that may be raised.
"""
msg = {}
if isinstance(args, dict):
msg = args
elif isinstance(args, list):
msg = dict(zip(inst.get_fields_and_field_types().keys(), args))

# Populate the provided instance, propagating any exceptions
populate_instance(msg, inst)

def start_goal(self, goal_msg):
if not self.client.action_client.wait_for_server(timeout_sec=10.0):
self.client.node_handle.get_logger().warning(
f" Timeout: Action Server for Client: {self.client.action_name} not available. Goal is ignored "
)
raise Exception("Action Server Not Available")

inst = get_action_goal_instance(self.client.action_type)
# Populate the goal instance with the provided goal args
self.args_to_action_goal_instance(inst, goal_msg)

# send the goal and wait for the goal future to be accepted
send_goal_future = self.client.action_client.send_goal_async(inst, self.feedback)
rclpy.spin_until_future_complete(self.client.node_handle, send_goal_future)
goal_handle = send_goal_future.result()
if not goal_handle.accepted:
raise Exception("Action Goal was rejected!")
self.client.node_handle.get_logger().info(
f"Goal is accepted by the action server: {self.client.action_name}."
)

# get the result
result = goal_handle.get_result()

# return the result of the goal if succeeded.
status = result.status
if status == GoalStatus.STATUS_SUCCEEDED:
# Turn the response into JSON and pass to the callback
json_response = extract_values(result.result)
else:
raise Exception(status)

return json_response
Loading
Loading