-
Notifications
You must be signed in to change notification settings - Fork 515
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
Closed
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 cfd3a95
changes status and goal inst to action_msgs
sathak93 f617d7e
added initial action client working
sathak93 a7a4775
added create client capability working
sathak93 e00d188
added action class to rosbridge _protocol
sathak93 e0029c4
protocol send improve
sathak93 7736a28
moved createclient to init
sathak93 4189200
call in a new thread
sathak93 28ab4e1
start action in new thread
sathak93 9b7dbd2
cleanup
sathak93 450314a
feedback as option
sathak93 bd847af
changes
sathak93 bc97e79
fix comma
sathak93 6a0482e
rename and changes in send msg
sathak93 5c25b33
cancel goal initial commit working
sathak93 88551ee
change logic starting thread
sathak93 c084318
unregister cancecl goal
sathak93 065d9f0
seperate action client
sathak93 c7e2863
linting fix
sathak93 e9345fd
linting cleanup
sathak93 82e3e43
linting cleanup rename
sathak93 291ff9e
rename
sathak93 d5a547c
lint
sathak93 f3650d9
fix action cancel service
sathak93 730d54f
added comment
sathak93 def764b
cancel goal through service call
sathak93 48851c7
cleanup
sathak93 0bb8249
revert unrelated changes
sathak93 f398b27
cancel before destroy
sathak93 c84b9bb
Merge branch 'RobotWebTools:ros2' into act2
sathak93 6367827
fix ci precommit errors
sathak93 18124f0
create Client per name
sathak93 8b0d2df
get_result instead of async
sathak93 9c0ca8e
added action spec to protocol
sathak93 44e78fd
fix missed
sathak93 d45eb8e
fix topo and json
sathak93 7dfec71
variable rename
sathak93 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
rosbridge_library/src/rosbridge_library/capabilities/action_client.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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): | ||
|
||
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
156
rosbridge_library/src/rosbridge_library/internal/actions.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?